diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f8df48..c2df080b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,21 @@ and this project adheres to ## [Unreleased] - FairPlay Utilities [#131](https://github.com/streaming-video-technology-alliance/common-media-library/issues/131) +## [0.9.0] - 2025-02-21 + +### Added + +- ISO BMFF parser and utilities + ([#118](https://github.com/streaming-video-technology-alliance/common-media-library/issues/118)) + +## [0.8.0] - 2025-02-14 + ### Added - Add DASH Segment Template Utility ([#129](https://github.com/streaming-video-technology-alliance/common-media-library/issues/129)) - Add XML parsing utils - [#125](https://github.com/streaming-video-technology-alliance/common-media-library/issues/125) + ([#125](https://github.com/streaming-video-technology-alliance/common-media-library/issues/125)) ### Changed @@ -260,7 +269,9 @@ and this project adheres to - Bootstrap project ([#2](https://github.com/streaming-video-technology-alliance/common-media-library/issues/2)) -[Unreleased]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.7.4...HEAD +[Unreleased]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.9.0...HEAD +[0.9.0]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.8.0...v0.9.0 +[0.8.0]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.7.4...v0.8.0 [0.7.4]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.7.3...v0.7.4 [0.7.3]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.7.2...v0.7.3 [0.7.2]: https://github.com/streaming-video-technology-alliance/common-media-library/compare/v0.7.1...v0.7.2 diff --git a/dev/isobmff.html b/dev/isobmff.html new file mode 100644 index 00000000..5e68650d --- /dev/null +++ b/dev/isobmff.html @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/dev/package.json b/dev/package.json index 8986cd4d..ae823e3f 100644 --- a/dev/package.json +++ b/dev/package.json @@ -1,7 +1,7 @@ { "name": "@svta/common-media-library-dev", "private": true, - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "homepage": "https://github.com/streaming-video-technology-alliance/common-media-library", "authors": "Casey Occhialini <1508707+littlespex@users.noreply.github.com>", diff --git a/docs/package.json b/docs/package.json index 15525285..82383a3d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "@svta/common-media-library-docs", "private": true, - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "homepage": "https://github.com/streaming-video-technology-alliance/common-media-library", "authors": "Casey Occhialini <1508707+littlespex@users.noreply.github.com>", diff --git a/jsconfig.json b/jsconfig.json index e6635787..690d1c1f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -9,7 +9,7 @@ "exclude": [ "**/node_modules", "**/dist", - "**/samples" + "**/samples" ], "include": [ "**/*.js" diff --git a/lib/NOTICE b/lib/NOTICE index 90c691f5..c3a33b65 100644 --- a/lib/NOTICE +++ b/lib/NOTICE @@ -449,3 +449,35 @@ SOFTWARE. ``` --- + +The following implementation in this project is derived from the +`codem-isoboxer` library (https://github.com/Dash-Industry-Forum/codem-isoboxer) + +- src/iso/bmff/* + +``` +Copyright (c) 2015 Hiro, Sjoerd Tieleman + +http://madebyhiro.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` + +--- diff --git a/lib/config/common-media-library.api.md b/lib/config/common-media-library.api.md index c2db0a9e..856af044 100644 --- a/lib/config/common-media-library.api.md +++ b/lib/config/common-media-library.api.md @@ -4,6 +4,37 @@ ```ts +// @alpha +export type AdaptationSet = { + $: { + audioSamplingRate?: string; + codecs?: string; + contentType?: string; + frameRate?: string; + group?: string; + id?: string; + lang?: string; + maxBandwidth?: string; + maxFrameRate?: string; + maxHeight?: string; + maxWidth?: string; + mimeType?: string; + minBandwidth?: string; + par?: string; + sar?: string; + segmentAlignment: string; + startWithSAP?: string; + subsegmentAlignment?: string; + subsegmentStartsWithSAP?: string; + }; + AudioChannelConfiguration?: AudioChannelConfiguration[]; + ContentComponent?: ContentComponent[]; + Role?: Role[]; + Representation: Representation[]; + SegmentTemplate?: SegmentTemplate[]; + SegmentList?: SegmentList[]; +}; + // @alpha export type AlignedSwitchingSet = { switchingSets: SwitchingSet[]; @@ -15,18 +46,81 @@ export function appendCmcdHeaders(headers: Record, cmcd: Cmcd, o // @beta export function appendCmcdQuery(url: string, cmcd: Cmcd, options?: CmcdEncodeOptions): string; +// @beta +export function ardi(view: IsoView): AudioRenderingIndicationBox; + +// @alpha +export type AudioChannelConfiguration = { + $: { + schemeIdUri: string; + value: string; + }; +}; + +// @beta +export type AudioRenderingIndicationBox = FullBox & { + audioRenderingIndication: number; +}; + +// @beta +export type AudioSampleEntry = SampleEntry & { + reserved2: number[]; + channelcount: number; + samplesize: number; + preDefined: number; + reserved3: number; + samplerate: number; + esds: Uint8Array; +}; + // @alpha export type AudioTrack = Track & { sampleRate: number; channels: number; }; +// @beta +export function avc1(view: IsoView): VisualSampleEntry; + +// @beta +export const avc2: BoxParser; + +// @beta +export const avc3: BoxParser; + +// @beta +export const avc4: BoxParser; + // @beta export function base64decode(str: string): Uint8Array; // @beta export function base64encode(binary: Uint8Array): string; +// @beta +export type Box = T & { + type: string; + size: number; + largesize?: number; + usertype?: number[]; + boxes?: Box[]; +}; + +// @beta +export type BoxFilter = (box: Box) => boolean; + +// @beta +export type BoxParser = (view: IsoView, config?: IsoViewConfig) => V; + +// @beta +export type BoxParserMap = Record; + +// @alpha +export type Byterange = { + length: number; + offset: number; +}; + // @beta export function canParseId3(data: Uint8Array, offset: number): boolean; @@ -316,6 +410,29 @@ export type CommonMediaResponse = { resourceTiming: ResourceTiming; }; +// @beta +export type CompositionTimeToSampleBox = FullBox & { + entryCount: number; + entries: CompositionTimeToSampleEntry[]; +}; + +// @beta +export type CompositionTimeToSampleEntry = { + sampleCount: number; + sampleOffset: number; +}; + +// @alpha (undocumented) +export type ContentComponent = { + $: { + contentType: string; + id: string; + }; +}; + +// @beta +export function createIsoView(raw: IsoData, config?: IsoViewConfig): IsoView; + // @beta export class Cta608Channel { constructor(channelNumber: number, outputFilter: CueHandler, logger?: CaptionsLogger); @@ -375,6 +492,9 @@ export class Cta608Parser { reset(): void; } +// @beta +export function ctts(view: IsoView): CompositionTimeToSampleBox; + // @beta export type CueHandler = { newCue(startTime: number, endTime: number, screen: CaptionScreen): void; @@ -400,6 +520,15 @@ export type DashManifest = { // @alpha export function dashToHam(manifest: string): Presentation[]; +// @beta +export const DATA = "data"; + +// @beta +export type DataReferenceBox = FullBox & { + entryCount: number; + entries: Box[]; +}; + // @beta export function decodeCmcd(cmcd: string): Cmcd; @@ -428,6 +557,47 @@ export function decodeSfItem(input: string, options?: SfDecodeOptions): SfItem; // @beta export function decodeSfList(input: string, options?: SfDecodeOptions): SfMember[]; +// @beta +export type DecodingTimeSample = { + sampleCount: number; + sampleDelta: number; +}; + +// @beta +export type DecodingTimeToSampleBox = FullBox & { + entryCount: number; + entries: DecodingTimeSample[]; +}; + +// @beta +export function dref(view: IsoView): DataReferenceBox; + +// @beta +export type EditListBox = FullBox & { + entryCount: number; + entries: EditListEntry[]; +}; + +// @beta +export type EditListEntry = { + segmentDuration: number; + mediaTime: number; + mediaRateInteger: number; + mediaRateFraction: number; +}; + +// @beta +export function elng(view: IsoView): ExtendedLanguageBox; + +// @beta +export function elst(view: IsoView): EditListBox; + +// @beta +export function emsg(view: IsoView): EventMessageBox; + +// @beta +export const enca: BoxParser; + // @beta export function encodeCmcd(cmcd: Cmcd, options?: CmcdEncodeOptions): string; @@ -455,9 +625,49 @@ export function encodeSfItem(value: SfBareItem, params?: SfParameters): string; // @beta export function encodeSfList(value: SfMember[], options?: SfEncodeOptions): string; +// @beta +export const encv: BoxParser; + +// @beta +export type Entity = { + entityId: number; +}; + +// @beta +export type EventMessageBox = FullBox & { + schemeIdUri: string; + value: string; + timescale: number; + presentationTime: number; + presentationTimeDelta: number; + eventDuration: number; + id: number; + messageData: Uint8Array; +}; + +// @beta +export type ExtendedLanguageBox = FullBox & { + extendedLanguage: string; +}; + // @beta export function extractCta608Data(raw: DataView, cta608Range: Array): Array>; +// @beta +export type FileTypeBox = TypeBox; + +// @beta +export function filterBoxes(raw: IsoData, config: IsoViewConfig, fn: BoxFilter): Box[]; + +// @beta +export function filterBoxesByType(type: string, raw: IsoData, config?: IsoViewConfig): Box[]; + +// @beta +export function findBox(raw: IsoData, config: IsoViewConfig, fn: BoxFilter): Box | null; + +// @beta +export function findBoxByType(type: string, raw: IsoData, config?: IsoViewConfig): Box | null; + // @beta export function findCta608Nalus(raw: DataView, startPos: number, size: number): Array>; @@ -467,12 +677,32 @@ export type FrameRate = { frameRateDenominator?: number; }; +// @beta +export function free(view: IsoView): FreeSpaceBox; + +// @beta +export type FreeSpaceBox = { + data: Uint8Array; +}; + +// @beta +export function frma(view: IsoView): OriginalFormatBox; + // @beta export function fromCmcdHeaders(headers: Record | Headers): Cmcd; // @beta export function fromCmcdQuery(query: string | URLSearchParams): Cmcd; +// @beta +export function ftyp(view: IsoView): FileTypeBox; + +// @beta +export type FullBox = { + version: number; + flags: number; +}; + // Warning: (ae-internal-missing-underscore) The name "getId3Data" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -504,6 +734,20 @@ export function hamToDash(presentation: Presentation[]): Manifest; // @alpha export function hamToHls(presentation: Presentation[]): Manifest; +// @beta +export type HandlerReferenceBox = { + preDefined: number; + handlerType: string; + reserved: number[]; + name: string; +}; + +// @beta +export function hdlr(view: IsoView): HandlerReferenceBox; + +// @beta +export const hev1: BoxParser; + // @alpha export type HlsManifest = { playlists: PlayList[]; @@ -515,14 +759,108 @@ export type HlsManifest = { // @alpha export function hlsToHam(manifest: string, ancillaryManifests: string[]): Presentation[]; +// @beta +export function hvc1(view: IsoView): VisualSampleEntry; + // @beta export type Id3Frame = DecodedId3Frame; +// @beta +export type IdentifiedMediaDataBox = { + imdaIdentifier: number; + data: Uint8Array; +}; + +// @beta +export function imda(view: IsoView): IdentifiedMediaDataBox; + +// @alpha +export type Initialization = { + $: { + range?: string; + sourceURL?: string; + }; +}; + +// @beta +export const INT = "int"; + // Warning: (ae-internal-missing-underscore) The name "isId3TimestampFrame" should be prefixed with an underscore because the declaration is marked as @internal // // @internal export function isId3TimestampFrame(frame: Id3Frame): boolean; +// @beta +export type IsoData = ArrayBuffer | DataView | IsoView | Uint8Array; + +// @beta +export type ISOFieldTypeMap = { + uint: number; + int: number; + template: number; + string: string; + data: Uint8Array; + utf8: string; + utf8string: string; +}; + +// @beta +export class IsoView { + // (undocumented) + [Symbol.iterator](): Generator; + constructor(raw: ArrayBuffer | DataView | Uint8Array, config?: IsoViewConfig); + // (undocumented) + get bytesRemaining(): number; + // (undocumented) + get cursor(): number; + // (undocumented) + get done(): boolean; + // (undocumented) + readArray: (type: T, size: number, length: number) => ISOFieldTypeMap[T][]; + // (undocumented) + readBox: () => RawBox; + // (undocumented) + readBoxes: (length: number) => Box[]; + // (undocumented) + readData: (size: number) => Uint8Array; + // (undocumented) + readEntries: (length: number, map: () => T) => T[]; + // (undocumented) + readFullBox: () => FullBox; + // (undocumented) + readInt: (size: number) => number; + // (undocumented) + readString: (size: number) => string; + // (undocumented) + readTemplate: (size: number) => number; + // (undocumented) + readUint: (size: number) => number; + // (undocumented) + readUtf8: (size?: number) => string; + // (undocumented) + slice: (size: number) => IsoView; +} + +// @beta +export type IsoViewConfig = { + parsers?: BoxParserMap; + recursive?: boolean; +}; + +// @beta +export function kind(view: IsoView): TrackKindBox; + +// @beta +export type LabelBox = FullBox & { + isGroupLabel: boolean; + labelId: number; + language: string; + label: string; +}; + +// @beta +export function labl(view: IsoView): LabelBox; + // @alpha export type Manifest = { manifest: string; @@ -535,6 +873,103 @@ export type Manifest = { // @alpha export type ManifestFormat = 'hls' | 'dash'; +// @beta +export function mdat(view: IsoView): MediaDataBox; + +// @beta +export function mdhd(view: IsoView): MediaHeaderBox; + +// @beta +export type MediaDataBox = { + data: Uint8Array; +}; + +// @alpha +export type MediaGroups = { + AUDIO: { + [key: string]: { + [key: string]: { + language: string; + }; + }; + }; + SUBTITLES: { + [key: string]: { + [key: string]: { + language: string; + }; + }; + }; +}; + +// @beta +export type MediaHeaderBox = FullBox & { + creationTime: number; + modificationTime: number; + timescale: number; + duration: number; + language: string; + preDefined: number; +}; + +// @beta +export function mehd(view: IsoView): MovieExtendsHeaderBox; + +// @beta +export function meta(view: IsoView): MetaBox; + +// @beta +export type MetaBox = FullBox; + +// @beta +export function mfhd(view: IsoView): MovieFragmentHeaderBox; + +// @beta +export function mfro(view: IsoView): MovieFragmentRandomAccessBox; + +// @beta +export type MovieExtendsHeaderBox = FullBox & { + fragmentDuration: number; +}; + +// @beta +export type MovieFragmentHeaderBox = FullBox & { + sequenceNumber: number; +}; + +// @beta +export type MovieFragmentRandomAccessBox = FullBox & { + mfra_size: number; +}; + +// @beta +export type MovieHeaderBox = { + version: number; + flags: number; + creationTime: number; + modificationTime: number; + timescale: number; + duration: number; + rate: number; + volume: number; + reserved1: number; + reserved2: number[]; + matrix: number[]; + preDefined: number[]; + nextTrackId: number; +}; + +// @beta +export function mp4a(view: IsoView): AudioSampleEntry; + +// @beta +export function mvhd(view: IsoView): MovieHeaderBox; + +// @beta +export type OriginalFormatBox = { + dataFormat: number; +}; + // @beta export type PACData = { row: number; @@ -544,9 +979,15 @@ export type PACData = { italics: boolean; }; +// @beta +export function parseBoxes(raw: IsoData, config?: IsoViewConfig): Box[]; + // @beta export function parseXml(input: string, options?: XmlParseOptions): XmlNode; +// @beta +export function payl(view: IsoView): WebVTTCuePayloadBox; + // @beta export class PenState { // (undocumented) @@ -580,14 +1021,122 @@ export type PenStyles = { flash: boolean; }; +// @alpha +export type Period = { + $: { + duration: string; + id?: string; + start?: string; + }; + AdaptationSet: AdaptationSet[]; +}; + +// @alpha +export type PlayList = { + uri: string; + attributes: { + FRAME_RATE: number; + CODECS: string; + BANDWIDTH: number; + RESOLUTION: { + width: number; + height: number; + }; + }; +}; + +// @beta +export type PreselectionGroupBox = FullBox & { + groupId: number; + numEntitiesInGroup: number; + entities: Entity[]; + preselectionTag?: string; + selectionPriority?: number; + interleavingTag?: string; +}; + // @alpha export type Presentation = Ham & { selectionSets: SelectionSet[]; }; +// @beta +export function prft(view: IsoView): ProducerReferenceTimeBox; + // @beta export function processUriTemplate(uriTemplate: string, representationId: string | null | undefined, number: number | null | undefined, subNumber: number | null | undefined, bandwidth: number | null | undefined, time: number | null | undefined): string; +// @beta +export type ProducerReferenceTimeBox = FullBox & { + referenceTrackId: number; + ntpTimestampSec: number; + ntpTimestampFrac: number; + mediaTime: number; +}; + +// @beta +export type ProtectionSystemSpecificHeaderBox = FullBox & { + systemID: number[]; + dataSize: number; + data: number[]; +}; + +// @beta +export function prsl(view: IsoView): PreselectionGroupBox; + +// @beta +export function pssh(view: IsoView): ProtectionSystemSpecificHeaderBox; + +// @beta +type Range_2 = { + level: number; + rangeSize: number; +}; +export { Range_2 as Range } + +// @beta +export type RawBox = { + type: string; + size: number; + largesize?: number; + usertype?: number[]; + data: IsoView; +}; + +// @beta +export type Reference = { + reference: number; + subsegmentDuration: number; + sap: number; + referenceType: number; + referencedSize: number; + startsWithSap: number; + sapType: number; + sapDeltaTime: number; +}; + +// @alpha +export type Representation = { + $: { + audioSamplingRate?: string; + bandwidth: string; + codecs?: string; + frameRate?: string; + height?: string; + id: string; + mimeType?: string; + sar?: string; + scanType?: string; + startWithSAP?: string; + width?: string; + }; + AudioChannelConfiguration?: AudioChannelConfiguration[]; + BaseURL?: string[]; + SegmentBase?: SegmentBase[]; + SegmentList?: SegmentList[]; + SegmentTemplate?: SegmentTemplate[]; +}; + // @beta export type RequestInterceptor = (request: CommonMediaRequest) => Promise; @@ -603,6 +1152,14 @@ export type ResourceTiming = { // @beta export type ResponseInterceptor = (response: CommonMediaResponse) => Promise; +// @alpha +export type Role = { + $: { + schemeIdUri: string; + value: string; + }; +}; + // @beta export function roundToEven(value: number, precision: number): number; @@ -636,6 +1193,23 @@ export class Row { setPenStyles(styles: Partial): void; } +// @beta +export type SampleDependencyTypeBox = FullBox & { + sampleDependencyTable: number[]; +}; + +// @beta +export type SampleDescriptionBox = FullBox & { + entryCount: number; + entries: any[]; +}; + +// @beta +export type SampleEntry = { + reserved1: number[]; + dataReferenceIndex: number; +}; + // @beta export class SccParser { constructor(processor: any, field?: number | any); @@ -661,6 +1235,19 @@ export class SccParser { timeConverter(smpteTs: string): number; } +// @beta +export type SchemeTypeBox = FullBox & { + schemeType: number; + schemeVersion: number; + schemeUri?: string; +}; + +// @beta +export function schm(view: IsoView): SchemeTypeBox; + +// @beta +export function sdtp(view: IsoView): SampleDependencyTypeBox; + // @alpha export type Segment = { duration: number; @@ -668,6 +1255,70 @@ export type Segment = { byteRange?: string; }; +// @alpha +export type SegmentBase = { + $: { + indexRange: string; + indexRangeExact: string; + timescale: string; + }; + Initialization: Initialization[]; +}; + +// @alpha +export type SegmentHls = { + title?: string; + duration: number; + byterange?: Byterange; + uri?: string; + timeline?: number; + map?: { + uri: string; + byterange: Byterange; + }; +}; + +// @beta +export type SegmentIndexBox = FullBox & { + referenceId: number; + timescale: number; + earliestPresentationTime: number; + firstOffset: number; + reserved: number; + references: Reference[]; +}; + +// @alpha +export type SegmentList = { + $: { + duration: string; + timescale: string; + }; + Initialization: Initialization[]; + SegmentURL?: SegmentURL[]; +}; + +// @alpha +export type SegmentTemplate = { + $: { + duration: string; + initialization: string; + media: string; + startNumber: string; + timescale: string; + }; +}; + +// @beta +export type SegmentTypeBox = TypeBox; + +// @alpha +export type SegmentURL = { + $: { + media?: string; + }; +}; + // @alpha export type SelectionSet = Ham & { switchingSets: SwitchingSet[]; @@ -736,6 +1387,42 @@ export class SfToken { description: string; } +// @beta +export function sidx(view: IsoView): SegmentIndexBox; + +// @beta +export const skip: BoxParser; + +// @beta +export function smhd(view: IsoView): SoundMediaHeaderBox; + +// @beta +export type SoundMediaHeaderBox = FullBox & { + balance: number; + reserved: number; +}; + +// @beta +export function ssix(view: IsoView): SubsegmentIndexBox; + +// @beta +export function sthd(view: IsoView): SubtitleMediaHeaderBox; + +// @beta +export const STRING = "string"; + +// @beta +export function stsd(view: IsoView): SampleDescriptionBox; + +// @beta +export function stss(view: IsoView): SyncSampleBox; + +// @beta +export function sttg(view: IsoView): WebVTTSettingsBox; + +// @beta +export function stts(view: IsoView): DecodingTimeToSampleBox; + // @beta export class StyledUnicodeChar { // (undocumented) @@ -756,6 +1443,48 @@ export class StyledUnicodeChar { uchar: string; } +// @beta +export const styp: BoxParser; + +// @beta +export function subs(view: IsoView): SubSampleInformationBox; + +// @beta +export type SubSample = { + subsampleSize: number; + subsamplePriority: number; + discardable: number; + codecSpecificParameters: number; +}; + +// @beta +export type SubSampleEntry = { + sampleDelta: number; + subsampleCount: number; + subsamples: SubSample[]; +}; + +// @beta +export type SubSampleInformationBox = FullBox & { + entryCount: number; + entries: SubSampleEntry[]; +}; + +// @beta +export type Subsegment = { + rangesCount: number; + ranges: Range_2[]; +}; + +// @beta +export type SubsegmentIndexBox = FullBox & { + subsegmentCount: number; + subsegments: Subsegment[]; +}; + +// @beta +export type SubtitleMediaHeaderBox = FullBox; + // @beta export type SupportedField = 1 | 3; @@ -764,10 +1493,39 @@ export type SwitchingSet = Ham & { tracks: Track[]; }; +// @beta +export type SyncSample = { + sampleNumber: number; +}; + +// @beta +export type SyncSampleBox = FullBox & { + entryCount: number; + entries: SyncSample[]; +}; + +// @beta +export const TEMPLATE = "template"; + +// @beta +export function tenc(view: IsoView): TrackEncryptionBox; + // @alpha type TextTrack_2 = Track; export { TextTrack_2 as TextTrack } +// @beta +export function tfdt(view: IsoView): TrackFragmentDecodeTimeBox; + +// @beta +export function tfhd(view: IsoView): TrackFragmentHeaderBox; + +// @beta +export function tfra(view: IsoView): TrackFragmentRandomAccessBox; + +// @beta +export function tkhd(view: IsoView): TrackHeaderBox; + // @beta export function toCmcdHeaders(cmcd: Cmcd, options?: CmcdEncodeOptions): Record; @@ -790,15 +1548,141 @@ export type Track = Ham & { segments: Segment[]; }; +// @beta +export type TrackEncryptionBox = FullBox & { + defaultIsEncrypted: number; + defaultIvSize: number; + defaultKid: number[]; +}; + +// @beta +export type TrackExtendsBox = FullBox & { + trackId: number; + defaultSampleDescriptionIndex: number; + defaultSampleDuration: number; + defaultSampleSize: number; + defaultSampleFlags: number; +}; + +// @beta +export type TrackFragmentDecodeTimeBox = FullBox & { + baseMediaDecodeTime: number; +}; + +// @beta +export type TrackFragmentHeaderBox = FullBox & { + trackId: number; + baseDataOffset?: number; + sampleDescriptionOffset?: number; + defaultSampleDuration?: number; + defaultSampleSize?: number; + defaultSampleFlags?: number; +}; + +// @beta +export type TrackFragmentRandomAccessBox = FullBox & { + trackId: number; + reserved: number; + numberOfEntry: number; + lengthSizeOfTrafNum: number; + lengthSizeOfTrunNum: number; + lengthSizeOfSampleNum: number; + entries: TrackFragmentRandomAccessEntry[]; +}; + +// @beta +export type TrackFragmentRandomAccessEntry = { + time: number; + moofOffset: number; + trafNumber: number; + trunNumber: number; + sampleNumber: number; +}; + +// @beta +export type TrackHeaderBox = FullBox & { + creationTime: number; + modificationTime: number; + trackId: number; + reserved1: number; + duration: number; + reserved2: number[]; + layer: number; + alternateGroup: number; + volume: number; + reserved3: number; + matrix: number[]; + width: number; + height: number; +}; + +// @beta +export type TrackKindBox = FullBox & { + schemeUri: string; + value: string; +}; + +// @beta +export type TrackRunBox = FullBox & { + sampleCount: number; + dataOffset?: number; + firstSampleFlags?: number; + samples: TrackRunSample[]; +}; + +// @beta +export type TrackRunSample = { + sampleDuration?: number; + sampleSize?: number; + sampleFlags?: number; + sampleCompositionTimeOffset?: number; +}; + // @alpha export type TrackType = 'audio' | 'video' | 'text'; +// @beta +export function trex(view: IsoView): TrackExtendsBox; + +// @beta +export function trun(view: IsoView): TrackRunBox; + +// @beta +export type TypeBox = { + majorBrand: string; + minorVersion: number; + compatibleBrands: string[]; +}; + +// @beta +export const UINT = "uint"; + // @beta export function unescapeHtml(text: string): string; +// @beta +export function url(view: IsoView): UrlBox; + +// @beta +export type UrlBox = FullBox & { + location: string; +}; + // @beta export function urlToRelativePath(url: string, base: string): string; +// @beta +export function urn(view: IsoView): UrnBox; + +// @beta +export type UrnBox = FullBox & { + name: string; + location: string; +}; + +// @beta +export const UTF8 = "utf8"; + // @beta export function utf8ArrayToStr(array: Uint8Array, exitOnNull?: boolean): string; @@ -851,6 +1735,12 @@ export const VerboseLevel: { // @beta (undocumented) export type VerboseLevel = ValueOf; +// @beta +export type VideoMediaHeaderBox = FullBox & { + graphicsmode: number; + opcolor: number[]; +}; + // @alpha export type VideoTrack = Track & { width: number; @@ -861,6 +1751,58 @@ export type VideoTrack = Track & { scanType: string; }; +// @beta +export type VisualSampleEntry = SampleEntry & { + preDefined1: number; + reserved2: number; + preDefined2: number[]; + width: number; + height: number; + horizresolution: number; + vertresolution: number; + reserved3: number; + frameCount: number; + compressorName: number[]; + depth: number; + preDefined3: number; + config: Uint8Array; +}; + +// @beta +export function vlab(view: IsoView): WebVTTSourceLabelBox; + +// @beta +export function vmhd(view: IsoView): VideoMediaHeaderBox; + +// @beta +export function vttC(view: IsoView): WebVTTConfigurationBox; + +// @beta +export function vtte(): WebVTTEmptySampleBox; + +// @beta +export type WebVTTConfigurationBox = { + config: string; +}; + +// @beta +export type WebVTTCuePayloadBox = { + cueText: string; +}; + +// @beta +export type WebVTTEmptySampleBox = object; + +// @beta +export type WebVTTSettingsBox = { + settings: string; +}; + +// @beta +export type WebVTTSourceLabelBox = { + sourceLabel: string; +}; + // @beta export type XmlNode = { nodeName: string; @@ -876,11 +1818,4 @@ export type XmlParseOptions = { keepComments?: boolean; }; -// Warnings were encountered during analysis: -// -// src/cmaf/ham/types/mapper/dash/DashManifest.ts:19:3 - (ae-forgotten-export) The symbol "Period" needs to be exported by the entry point index.d.ts -// src/cmaf/ham/types/mapper/hls/HlsManifest.ts:12:2 - (ae-forgotten-export) The symbol "PlayList" needs to be exported by the entry point index.d.ts -// src/cmaf/ham/types/mapper/hls/HlsManifest.ts:13:2 - (ae-forgotten-export) The symbol "MediaGroups" needs to be exported by the entry point index.d.ts -// src/cmaf/ham/types/mapper/hls/HlsManifest.ts:14:2 - (ae-forgotten-export) The symbol "SegmentHls" needs to be exported by the entry point index.d.ts - ``` diff --git a/lib/package.json b/lib/package.json index 36d681cd..04333315 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "@svta/common-media-library", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "homepage": "https://github.com/streaming-video-technology-alliance/common-media-library", "authors": "Casey Occhialini <1508707+littlespex@users.noreply.github.com>", diff --git a/lib/src/cmaf-ham.ts b/lib/src/cmaf-ham.ts index f3dd0772..4280bb80 100644 --- a/lib/src/cmaf-ham.ts +++ b/lib/src/cmaf-ham.ts @@ -20,8 +20,23 @@ export type { VideoTrack } from './cmaf/ham/types/model/VideoTrack.js'; export type { Manifest } from './cmaf/ham/types/manifest/Manifest.js'; export type { ManifestFormat } from './cmaf/ham/types/manifest/ManifestFormat.js'; +export type { AdaptationSet } from './cmaf/ham/types/mapper/dash/AdaptationSet.js'; +export type { AudioChannelConfiguration } from './cmaf/ham/types/mapper/dash/AudioChannelConfiguration.js'; +export type { ContentComponent } from './cmaf/ham/types/mapper/dash/ContentComponent.js'; export type { DashManifest } from './cmaf/ham/types/mapper/dash/DashManifest.js'; +export type { Initialization } from './cmaf/ham/types/mapper/dash/Initialization.js'; +export type { Period } from './cmaf/ham/types/mapper/dash/Period.js'; +export type { Representation } from './cmaf/ham/types/mapper/dash/Representation.js'; +export type { Role } from './cmaf/ham/types/mapper/dash/Role.js'; +export type { SegmentBase } from './cmaf/ham/types/mapper/dash/SegmentBase.js'; +export type { SegmentList } from './cmaf/ham/types/mapper/dash/SegmentList.js'; +export type { SegmentTemplate } from './cmaf/ham/types/mapper/dash/SegmentTemplate.js'; +export type { SegmentURL } from './cmaf/ham/types/mapper/dash/SegmentUrl.js'; +export type { Byterange } from './cmaf/ham/types/mapper/hls/Byterange.js'; export type { HlsManifest } from './cmaf/ham/types/mapper/hls/HlsManifest.js'; +export type { MediaGroups } from './cmaf/ham/types/mapper/hls/MediaGroups.js'; +export type { PlayList } from './cmaf/ham/types/mapper/hls/Playlist.js'; +export type { SegmentHls } from './cmaf/ham/types/mapper/hls/SegmentHls.js'; export type { Validation } from './cmaf/ham/types/Validation.js'; export { setDashParser } from './cmaf/ham/utils/dash/parseDashManifest.js'; diff --git a/lib/src/cmaf/ham/types/mapper/dash/ContentComponent.ts b/lib/src/cmaf/ham/types/mapper/dash/ContentComponent.ts index 546ea1bb..9fa59d9b 100644 --- a/lib/src/cmaf/ham/types/mapper/dash/ContentComponent.ts +++ b/lib/src/cmaf/ham/types/mapper/dash/ContentComponent.ts @@ -1,3 +1,8 @@ +/** + * @group CMAF + * + * @alpha + */ export type ContentComponent = { $: { contentType: string; diff --git a/lib/src/cmaf/ham/types/mapper/dash/Role.ts b/lib/src/cmaf/ham/types/mapper/dash/Role.ts index 972abb40..84d61307 100644 --- a/lib/src/cmaf/ham/types/mapper/dash/Role.ts +++ b/lib/src/cmaf/ham/types/mapper/dash/Role.ts @@ -1,3 +1,9 @@ +/** + * Role + * + * @group CMAF + * @alpha + */ export type Role = { $: { schemeIdUri: string; diff --git a/lib/src/cmaf/ham/types/mapper/hls/Byterange.ts b/lib/src/cmaf/ham/types/mapper/hls/Byterange.ts index 4d056336..a1b5f887 100644 --- a/lib/src/cmaf/ham/types/mapper/hls/Byterange.ts +++ b/lib/src/cmaf/ham/types/mapper/hls/Byterange.ts @@ -1 +1,8 @@ +/** + * Byterange + * + * @group CMAF + * + * @alpha + */ export type Byterange = { length: number; offset: number }; diff --git a/lib/src/id3/util/decodeId3ImageFrame.ts b/lib/src/id3/util/decodeId3ImageFrame.ts index 3bda888a..84fe56c6 100644 --- a/lib/src/id3/util/decodeId3ImageFrame.ts +++ b/lib/src/id3/util/decodeId3ImageFrame.ts @@ -1,8 +1,8 @@ +import { utf8ArrayToStr } from '../../utils.js'; import type { DecodedId3Frame } from '../DecodedId3Frame.js'; import type { RawId3Frame } from './RawFrame.js'; -import { toUint8 } from './utf8.js'; import { toArrayBuffer } from './toArrayBuffer.js'; -import { utf8ArrayToStr } from '../../utils.js'; +import { toUint8 } from './utf8.js'; type MetadataFrame = { key: string; @@ -54,7 +54,7 @@ export function decodeId3ImageFrame( data = utf8ArrayToStr( toUint8(frame.data, 4 + mimeTypeEndIndex + descriptionEndIndex), ); - } + } else { data = toArrayBuffer( frame.data.subarray(4 + mimeTypeEndIndex + descriptionEndIndex), diff --git a/lib/src/index.ts b/lib/src/index.ts index ce33db4c..b9792f1a 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -11,6 +11,7 @@ export type * from './cta.js'; export * from './dash.js'; export * from './id3.js'; export * from './iso8601.js'; +export * from './isobmff.js'; export type * from './request.js'; export * from './structuredfield.js'; export * from './utils.js'; diff --git a/lib/src/iso/bmff/Box.ts b/lib/src/iso/bmff/Box.ts new file mode 100644 index 00000000..898e3123 --- /dev/null +++ b/lib/src/iso/bmff/Box.ts @@ -0,0 +1,14 @@ +/** + * Box + * + * @group ISOBMFF + * + * @beta + */ +export type Box = T & { + type: string; + size: number; + largesize?: number; + usertype?: number[]; + boxes?: Box[]; +} diff --git a/lib/src/iso/bmff/BoxFilter.ts b/lib/src/iso/bmff/BoxFilter.ts new file mode 100644 index 00000000..cca423ec --- /dev/null +++ b/lib/src/iso/bmff/BoxFilter.ts @@ -0,0 +1,10 @@ +import type { Box } from './Box.js'; + +/** + * BoxFilter + * + * @group ISOBMFF + * + * @beta + */ +export type BoxFilter = (box: Box) => boolean; diff --git a/lib/src/iso/bmff/BoxParser.ts b/lib/src/iso/bmff/BoxParser.ts new file mode 100644 index 00000000..599c5104 --- /dev/null +++ b/lib/src/iso/bmff/BoxParser.ts @@ -0,0 +1,11 @@ +import type { IsoView } from './IsoView.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +/** + * Box parser + * + * @group ISOBMFF + * + * @beta + */ +export type BoxParser = (view: IsoView, config?: IsoViewConfig) => V; diff --git a/lib/src/iso/bmff/BoxParserMap.ts b/lib/src/iso/bmff/BoxParserMap.ts new file mode 100644 index 00000000..61a68567 --- /dev/null +++ b/lib/src/iso/bmff/BoxParserMap.ts @@ -0,0 +1,10 @@ +import type { BoxParser } from './BoxParser.js'; + +/** + * A map of box parsers to their box types + * + * @group ISOBMFF + * + * @beta + */ +export type BoxParserMap = Record; diff --git a/lib/src/iso/bmff/ContainerBoxes.ts b/lib/src/iso/bmff/ContainerBoxes.ts new file mode 100644 index 00000000..23da11df --- /dev/null +++ b/lib/src/iso/bmff/ContainerBoxes.ts @@ -0,0 +1,25 @@ +export const ContainerBoxes: string[] = [ + 'dinf', + 'edts', + 'enca', + 'encv', + 'grpl', + 'mdia', + 'meco', + 'meta', + 'mfra', + 'minf', + 'moof', + 'moov', + 'mvex', + 'prsl', + 'schi', + 'sinf', + 'stbl', + 'strk', + 'traf', + 'trak', + 'tref', + 'udta', + 'vttc', +]; diff --git a/lib/src/iso/bmff/FullBox.ts b/lib/src/iso/bmff/FullBox.ts new file mode 100644 index 00000000..2b95fe0f --- /dev/null +++ b/lib/src/iso/bmff/FullBox.ts @@ -0,0 +1,11 @@ +/** + * FullBox + * + * @group ISOBMFF + * + * @beta + */ +export type FullBox = { + version: number; + flags: number; +} diff --git a/lib/src/iso/bmff/IsoData.ts b/lib/src/iso/bmff/IsoData.ts new file mode 100644 index 00000000..7094d0a6 --- /dev/null +++ b/lib/src/iso/bmff/IsoData.ts @@ -0,0 +1,10 @@ +import type { IsoView } from './IsoView.js'; + +/** + * ISO data + * + * @group ISOBMFF + * + * @beta + */ +export type IsoData = ArrayBuffer | DataView | IsoView | Uint8Array; diff --git a/lib/src/iso/bmff/IsoView.ts b/lib/src/iso/bmff/IsoView.ts new file mode 100644 index 00000000..5e0d93b7 --- /dev/null +++ b/lib/src/iso/bmff/IsoView.ts @@ -0,0 +1,261 @@ +import type { Box } from './Box.js'; +import { ContainerBoxes } from './ContainerBoxes.js'; +import { DATA } from './fields/DATA.js'; +import { INT } from './fields/INT.js'; +import { STRING } from './fields/STRING.js'; +import { TEMPLATE } from './fields/TEMPLATE.js'; +import { UINT } from './fields/UINT.js'; +import { UTF8 } from './fields/UTF8.js'; +import type { FullBox } from './FullBox.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; +import type { ISOFieldTypeMap } from './readers/ISOFieldTypeMap.js'; +import { readData } from './readers/readData.js'; +import { readInt } from './readers/readInt.js'; +import { readString } from './readers/readString.js'; +import { readTemplate } from './readers/readTemplate.js'; +import { readTerminatedString } from './readers/readTerminatedString.js'; +import { readUint } from './readers/readUint.js'; +import { readUTF8String } from './readers/readUTF8String.js'; +import { readUTF8TerminatedString } from './readers/readUTF8TerminatedString.js'; + +/** + * Raw ISO BMFF data box. + * + * @group ISOBMFF + * + * @beta + */ +export type RawBox = { + type: string; + size: number; + largesize?: number; + usertype?: number[]; + data: IsoView; +} + +/** + * ISO BMFF data view. Similar to DataView, but with additional methods for reading ISO BMFF data. + * It implements the iterator protocol, so it can be used in a for...of loop. + * + * @group ISOBMFF + * + * @beta + */ +export class IsoView { + private dataView: DataView; + private offset: number; + private config: IsoViewConfig; + private truncated: boolean = false; + + constructor(raw: ArrayBuffer | DataView | Uint8Array, config?: IsoViewConfig) { + this.dataView = (raw instanceof ArrayBuffer) ? new DataView(raw) : (raw instanceof Uint8Array) ? new DataView(raw.buffer, raw.byteOffset, raw.byteLength) : raw; + this.offset = this.dataView.byteOffset; + this.config = config || { recursive: false, parsers: {} }; + } + + get cursor(): number { + return this.offset - this.dataView.byteOffset; + } + + get done(): boolean { + return this.cursor >= this.dataView.byteLength || this.truncated; + } + + get bytesRemaining(): number { + return this.dataView.byteLength - this.cursor; + } + + slice = (size: number): IsoView => { + const dataView = new DataView(this.dataView.buffer, this.offset, size); + this.offset += size; + return new IsoView(dataView, this.config); + }; + + private read = (type: T, size: number = 0): ISOFieldTypeMap[T] => { + // TODO: Change all sizes from bits to bytes + const { dataView, offset } = this; + + let result: any; + let cursor = size; + + switch (type) { + case UINT: + result = readUint(dataView, offset, size); + break; + + case INT: + result = readInt(dataView, offset, size); + break; + + case TEMPLATE: + result = readTemplate(dataView, offset, size); + break; + + case STRING: + if (size === -1) { + result = readTerminatedString(dataView, offset); + cursor = result.length + 1; + } + else { + result = readString(dataView, offset, size); + } + break; + + case DATA: + result = readData(dataView, offset, size); + cursor = result.length; + break; + + case UTF8: + if (size === -1) { + result = readUTF8TerminatedString(dataView, offset); + cursor = result.length + 1; + } + else { + result = readUTF8String(dataView, offset); + } + break; + + default: + result = -1; + } + + this.offset += cursor; + + return result; + }; + + readUint = (size: number): number => { + return this.read(UINT, size); + }; + + readInt = (size: number): number => { + return this.read(INT, size); + }; + + readString = (size: number): string => { + return this.read(STRING, size); + }; + + readTemplate = (size: number): number => { + return this.read(TEMPLATE, size); + }; + + readData = (size: number): Uint8Array => { + return this.read(DATA, size); + }; + + readUtf8 = (size?: number): string => { + return this.read(UTF8, size); + }; + + readFullBox = (): FullBox => { + return { + version: this.readUint(1), + flags: this.readUint(3), + }; + }; + + readArray = (type: T, size: number, length: number): ISOFieldTypeMap[T][] => { + const value = []; + + for (let i = 0; i < length; i++) { + value.push(this.read(type, size)); + } + + return value as ISOFieldTypeMap[T][]; + }; + + readBox = (): RawBox => { + const { dataView, offset } = this; + + // read box size and type without advancing the cursor in case the box is truncated + let cursor = 0; + + const box = { + size: readUint(dataView, offset, 4), + type: readString(dataView, offset + 4, 4), + } as RawBox; + + cursor += 8; + + if (box.size === 1) { + box.largesize = readUint(dataView, offset + cursor, 8); + cursor += 8; + } + + const actualSize = box.largesize || box.size; + if (this.cursor + actualSize > dataView.byteLength) { + this.truncated = true; + throw new Error('Truncated box'); + } + + this.offset += cursor; + if (box.type === 'uuid') { + box.usertype = this.readArray('uint', 1, 16); + } + + const viewSize = box.size === 0 ? this.bytesRemaining : actualSize - cursor; + box.data = this.slice(viewSize); + + return box; + }; + + readBoxes = (length: number): Box[] => { + const result: Box[] = []; + + for (const box of this) { + result.push(box); + + if (length > 0 && result.length >= length) { + break; + } + } + + return result; + }; + + readEntries = (length: number, map: () => T): T[] => { + const result: T[] = []; + + for (let i = 0; i < length; i++) { + result.push(map()); + } + + return result; + }; + + *[Symbol.iterator](): Generator { + const { parsers = {}, recursive = false } = this.config; + + while (!this.done) { + try { + const { type, data, ...rest } = this.readBox(); + const box = { type, ...rest } as Box; + const parser = parsers[type] || parsers[type.trim()]; // url and urn boxes have a trailing space in their type field + if (parser) { + Object.assign(box, parser(data, this.config)); + } + + if (ContainerBoxes.includes(type)) { + const boxes = []; + + for (const child of data) { + if (recursive) { + yield child; + } + + boxes.push(child); + } + + box.boxes = boxes; + } + + yield box; + } + catch (error) { + break; + } + } + } +} diff --git a/lib/src/iso/bmff/IsoViewConfig.ts b/lib/src/iso/bmff/IsoViewConfig.ts new file mode 100644 index 00000000..ff380cf0 --- /dev/null +++ b/lib/src/iso/bmff/IsoViewConfig.ts @@ -0,0 +1,20 @@ +import type { BoxParserMap } from './BoxParserMap.js'; + +/** + * ISO View configuration + * + * @group ISOBMFF + * + * @beta + */ +export type IsoViewConfig = { + /** + * A map of box parsers to their box types + */ + parsers?: BoxParserMap; + + /** + * Whether to parse boxes recursively + */ + recursive?: boolean; +}; diff --git a/lib/src/iso/bmff/TypeBox.ts b/lib/src/iso/bmff/TypeBox.ts new file mode 100644 index 00000000..8c1a57f0 --- /dev/null +++ b/lib/src/iso/bmff/TypeBox.ts @@ -0,0 +1,12 @@ +/** + * TypeBox + * + * @group ISOBMFF + * + * @beta + */ +export type TypeBox = { + majorBrand: string; + minorVersion: number; + compatibleBrands: string[]; +}; diff --git a/lib/src/iso/bmff/createIsoView.ts b/lib/src/iso/bmff/createIsoView.ts new file mode 100644 index 00000000..e6aea791 --- /dev/null +++ b/lib/src/iso/bmff/createIsoView.ts @@ -0,0 +1,19 @@ +import type { IsoData } from './IsoData.js'; +import { IsoView } from './IsoView.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +/** + * Create an IsoView from a raw ISO data. + * + * @param raw - The raw ISO data + * @param config - The configuration for the IsoView + * + * @returns The created IsoView + * + * @group ISOBMFF + * + * @beta + */ +export function createIsoView(raw: IsoData, config?: IsoViewConfig): IsoView { + return raw instanceof IsoView ? raw : new IsoView(raw, config); +} diff --git a/lib/src/iso/bmff/fields/DATA.ts b/lib/src/iso/bmff/fields/DATA.ts new file mode 100644 index 00000000..91c233a3 --- /dev/null +++ b/lib/src/iso/bmff/fields/DATA.ts @@ -0,0 +1,8 @@ +/** + * The data field type + * + * @group ISOBMFF + * + * @beta + */ +export const DATA = 'data'; diff --git a/lib/src/iso/bmff/fields/INT.ts b/lib/src/iso/bmff/fields/INT.ts new file mode 100644 index 00000000..b925dfa9 --- /dev/null +++ b/lib/src/iso/bmff/fields/INT.ts @@ -0,0 +1,8 @@ +/** + * The integer field type + * + * @group ISOBMFF + * + * @beta + */ +export const INT = 'int'; diff --git a/lib/src/iso/bmff/fields/STRING.ts b/lib/src/iso/bmff/fields/STRING.ts new file mode 100644 index 00000000..fa874357 --- /dev/null +++ b/lib/src/iso/bmff/fields/STRING.ts @@ -0,0 +1,8 @@ +/** + * The string field type + * + * @group ISOBMFF + * + * @beta + */ +export const STRING = 'string'; diff --git a/lib/src/iso/bmff/fields/TEMPLATE.ts b/lib/src/iso/bmff/fields/TEMPLATE.ts new file mode 100644 index 00000000..cfda14db --- /dev/null +++ b/lib/src/iso/bmff/fields/TEMPLATE.ts @@ -0,0 +1,8 @@ +/** + * The template field type + * + * @group ISOBMFF + * + * @beta + */ +export const TEMPLATE = 'template'; diff --git a/lib/src/iso/bmff/fields/UINT.ts b/lib/src/iso/bmff/fields/UINT.ts new file mode 100644 index 00000000..a53bcd4c --- /dev/null +++ b/lib/src/iso/bmff/fields/UINT.ts @@ -0,0 +1,8 @@ +/** + * The unsigned integer field type + * + * @group ISOBMFF + * + * @beta + */ +export const UINT = 'uint'; diff --git a/lib/src/iso/bmff/fields/UTF8.ts b/lib/src/iso/bmff/fields/UTF8.ts new file mode 100644 index 00000000..3717c60a --- /dev/null +++ b/lib/src/iso/bmff/fields/UTF8.ts @@ -0,0 +1,8 @@ +/** + * The UTF8 field type + * + * @group ISOBMFF + * + * @beta + */ +export const UTF8 = 'utf8'; diff --git a/lib/src/iso/bmff/filterBoxes.ts b/lib/src/iso/bmff/filterBoxes.ts new file mode 100644 index 00000000..202411bc --- /dev/null +++ b/lib/src/iso/bmff/filterBoxes.ts @@ -0,0 +1,34 @@ +import type { Box } from './Box.js'; +import type { BoxFilter } from './BoxFilter.js'; +import { createIsoView } from './createIsoView.js'; +import type { IsoData } from './IsoData.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +function filter(iterator: Iterable, recursive: boolean, fn: BoxFilter, boxes: Box[] = []): Box[] { + for (const box of iterator) { + if (fn(box)) { + boxes.push(box); + } + + if (recursive && Array.isArray(box.boxes)) { + filter(box.boxes, recursive, fn, boxes); + } + } + + return boxes; +} + +/** + * Filters boxes based on the given filter function. + * + * @param raw - The raw boxes to filter. + * @param config - The box parser configuration. + * @param fn - The filter function. + * @returns The filtered boxes. + * + * @group ISOBMFF + * @beta + */ +export function filterBoxes(raw: IsoData, config: IsoViewConfig, fn: BoxFilter): Box[] { + return filter(createIsoView(raw, { ...config, recursive: false }), !!config.recursive, fn); +} diff --git a/lib/src/iso/bmff/filterBoxesByType.ts b/lib/src/iso/bmff/filterBoxesByType.ts new file mode 100644 index 00000000..07fe72c3 --- /dev/null +++ b/lib/src/iso/bmff/filterBoxesByType.ts @@ -0,0 +1,21 @@ +import type { Box } from './Box.js'; +import { filterBoxes } from './filterBoxes.js'; +import type { IsoData } from './IsoData.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +/** + * Filter boxes by type from an IsoView + * + * @param type - The type of boxes to filter + * @param raw - The raw ISO data + * @param config - The configuration for the IsoView + * + * @returns The filtered boxes + * + * @group ISOBMFF + * + * @beta + */ +export function filterBoxesByType(type: string, raw: IsoData, config: IsoViewConfig = {}): Box[] { + return filterBoxes(raw, config, box => box.type === type); +} diff --git a/lib/src/iso/bmff/findBox.ts b/lib/src/iso/bmff/findBox.ts new file mode 100644 index 00000000..1d8960c4 --- /dev/null +++ b/lib/src/iso/bmff/findBox.ts @@ -0,0 +1,40 @@ +import type { Box } from './Box.js'; +import type { BoxFilter } from './BoxFilter.js'; +import { createIsoView } from './createIsoView.js'; +import type { IsoData } from './IsoData.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +function find(iterator: Iterable, recursive: boolean, fn: BoxFilter): Box | null { + for (const box of iterator) { + if (fn(box)) { + return box; + } + + if (recursive && Array.isArray(box.boxes)) { + const result = find(box.boxes, recursive, fn); + + if (result) { + return result; + } + } + } + + return null; +} + +/** + * Find a box from an IsoView that matches a filter function + * + * @param raw - The raw ISO data + * @param config - The configuration for the IsoView + * @param fn - The filter function + * + * @returns The first box that matches the filter function + * + * @group ISOBMFF + * + * @beta + */ +export function findBox(raw: IsoData, config: IsoViewConfig, fn: BoxFilter): Box | null { + return find(createIsoView(raw, { ...config, recursive: false }), !!config.recursive, fn); +} diff --git a/lib/src/iso/bmff/findBoxByType.ts b/lib/src/iso/bmff/findBoxByType.ts new file mode 100644 index 00000000..213ac943 --- /dev/null +++ b/lib/src/iso/bmff/findBoxByType.ts @@ -0,0 +1,21 @@ +import type { Box } from './Box.js'; +import { findBox } from './findBox.js'; +import type { IsoData } from './IsoData.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +/** + * Find a box from an IsoView that matches a given type + * + * @param type - The type of box to find + * @param raw - The raw ISO data + * @param config - The configuration for the IsoView + * + * @returns The first box that matches the type + * + * @group ISOBMFF + * + * @beta + */ +export function findBoxByType(type: string, raw: IsoData, config: IsoViewConfig = {}): Box | null { + return findBox(raw, config, box => box.type === type); +} diff --git a/lib/src/iso/bmff/parseBoxes.ts b/lib/src/iso/bmff/parseBoxes.ts new file mode 100644 index 00000000..f23254f7 --- /dev/null +++ b/lib/src/iso/bmff/parseBoxes.ts @@ -0,0 +1,26 @@ +import type { Box } from './Box.js'; +import { createIsoView } from './createIsoView.js'; +import type { IsoData } from './IsoData.js'; +import type { IsoViewConfig } from './IsoViewConfig.js'; + +/** + * Parse boxes from an IsoView + * + * @param raw - The raw ISO data + * @param config - The configuration for the IsoView + * + * @returns The parsed boxes + * + * @group ISOBMFF + * + * @beta + */ +export function parseBoxes(raw: IsoData, config?: IsoViewConfig): Box[] { + const boxes = []; + + for (const box of createIsoView(raw, config)) { + boxes.push(box); + } + + return boxes; +} diff --git a/lib/src/iso/bmff/parsers.ts b/lib/src/iso/bmff/parsers.ts new file mode 100644 index 00000000..3082aa6e --- /dev/null +++ b/lib/src/iso/bmff/parsers.ts @@ -0,0 +1,59 @@ +export * from './parsers/ardi.js'; +export * from './parsers/avc1.js'; +export * from './parsers/avc2.js'; +export * from './parsers/avc3.js'; +export * from './parsers/avc4.js'; +export * from './parsers/ctts.js'; +export * from './parsers/dref.js'; +export * from './parsers/elng.js'; +export * from './parsers/elst.js'; +export * from './parsers/emsg.js'; +export * from './parsers/enca.js'; +export * from './parsers/encv.js'; +export * from './parsers/free.js'; +export * from './parsers/frma.js'; +export * from './parsers/ftyp.js'; +export * from './parsers/hdlr.js'; +export * from './parsers/hev1.js'; +export * from './parsers/hvc1.js'; +export * from './parsers/imda.js'; +export * from './parsers/kind.js'; +export * from './parsers/labl.js'; +export * from './parsers/mdat.js'; +export * from './parsers/mdhd.js'; +export * from './parsers/mehd.js'; +export * from './parsers/meta.js'; +export * from './parsers/mfhd.js'; +export * from './parsers/mfro.js'; +export * from './parsers/mp4a.js'; +export * from './parsers/mvhd.js'; +export * from './parsers/payl.js'; +export * from './parsers/prft.js'; +export * from './parsers/prsl.js'; +export * from './parsers/pssh.js'; +export * from './parsers/schm.js'; +export * from './parsers/sdtp.js'; +export * from './parsers/sidx.js'; +export * from './parsers/skip.js'; +export * from './parsers/smhd.js'; +export * from './parsers/ssix.js'; +export * from './parsers/sthd.js'; +export * from './parsers/stsd.js'; +export * from './parsers/stss.js'; +export * from './parsers/sttg.js'; +export * from './parsers/stts.js'; +export * from './parsers/styp.js'; +export * from './parsers/subs.js'; +export * from './parsers/tenc.js'; +export * from './parsers/tfdt.js'; +export * from './parsers/tfhd.js'; +export * from './parsers/tfra.js'; +export * from './parsers/tkhd.js'; +export * from './parsers/trex.js'; +export * from './parsers/trun.js'; +export * from './parsers/url.js'; +export * from './parsers/urn.js'; +export * from './parsers/vlab.js'; +export * from './parsers/vmhd.js'; +export * from './parsers/vttC.js'; +export * from './parsers/vtte.js'; diff --git a/lib/src/iso/bmff/parsers/ardi.ts b/lib/src/iso/bmff/parsers/ardi.ts new file mode 100644 index 00000000..b1c8b01e --- /dev/null +++ b/lib/src/iso/bmff/parsers/ardi.ts @@ -0,0 +1,31 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:202x - 12.2.8 Audio rendering indication box + * + * @group ISOBMFF + * + * @beta + */ +export type AudioRenderingIndicationBox = FullBox & { + audioRenderingIndication: number; +} + +/** + * Parse a AudioRenderingIndicationBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed AudioRenderingIndicationBox + * + * @group ISOBMFF + * + * @beta + */ +export function ardi(view: IsoView): AudioRenderingIndicationBox { + return { + ...view.readFullBox(), + audioRenderingIndication: view.readUint(1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/avc1.ts b/lib/src/iso/bmff/parsers/avc1.ts new file mode 100644 index 00000000..95a3d2a7 --- /dev/null +++ b/lib/src/iso/bmff/parsers/avc1.ts @@ -0,0 +1,70 @@ +import { UINT } from '../fields/UINT.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2015 - 8.5.2.2 Sample Entry + * + * @group ISOBMFF + * + * @beta + */ +export type SampleEntry = { + reserved1: number[]; + dataReferenceIndex: number; +} + +/** + * ISO/IEC 14496-15:2014 - 12.1.3.1 avc1/2/3/4, hev1, hvc1, encv + * + * @group ISOBMFF + * + * @beta + */ +export type VisualSampleEntry = SampleEntry & { + preDefined1: number; + reserved2: number; + preDefined2: number[]; + width: number; + height: number; + horizresolution: number; + vertresolution: number; + reserved3: number; + frameCount: number; + compressorName: number[]; + depth: number; + preDefined3: number; + config: Uint8Array; +} + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export function avc1(view: IsoView): VisualSampleEntry { + const { readArray, readUint, readInt, readTemplate, readData } = view; + + return { + reserved1: readArray(UINT, 1, 6), + dataReferenceIndex: readUint(2), + preDefined1: readUint(2), + reserved2: readUint(2), + preDefined2: readArray(UINT, 4, 3), + width: readUint(2), + height: readUint(2), + horizresolution: readTemplate(4), + vertresolution: readTemplate(4), + reserved3: readUint(4), + frameCount: readUint(2), + compressorName: readArray(UINT, 1, 32), + depth: readUint(2), + preDefined3: readInt(2), + config: readData(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/avc2.ts b/lib/src/iso/bmff/parsers/avc2.ts new file mode 100644 index 00000000..4d988082 --- /dev/null +++ b/lib/src/iso/bmff/parsers/avc2.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export const avc2: BoxParser = avc1; diff --git a/lib/src/iso/bmff/parsers/avc3.ts b/lib/src/iso/bmff/parsers/avc3.ts new file mode 100644 index 00000000..85c37f54 --- /dev/null +++ b/lib/src/iso/bmff/parsers/avc3.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export const avc3: BoxParser = avc1; diff --git a/lib/src/iso/bmff/parsers/avc4.ts b/lib/src/iso/bmff/parsers/avc4.ts new file mode 100644 index 00000000..2e54e392 --- /dev/null +++ b/lib/src/iso/bmff/parsers/avc4.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export const avc4: BoxParser = avc1; diff --git a/lib/src/iso/bmff/parsers/ctts.ts b/lib/src/iso/bmff/parsers/ctts.ts new file mode 100644 index 00000000..afd9e3b1 --- /dev/null +++ b/lib/src/iso/bmff/parsers/ctts.ts @@ -0,0 +1,55 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * A Composition Time To Sample Entry + * + * @group ISOBMFF + * + * @beta + */ +export type CompositionTimeToSampleEntry = { + sampleCount: number; + sampleOffset: number; +} + +/** + * ISO/IEC 14496-12:2012 - 8.6.1.3 Composition Time To Sample Box + * + * @group ISOBMFF + * + * @beta + */ +export type CompositionTimeToSampleBox = FullBox & { + entryCount: number; + entries: CompositionTimeToSampleEntry[]; +}; + +/** + * Parse a CompositionTimeToSampleBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed CompositionTimeToSampleBox + * + * @group ISOBMFF + * + * @beta + */ +export function ctts(view: IsoView): CompositionTimeToSampleBox { + const { version, flags } = view.readFullBox(); + const read = version === 1 ? view.readInt : view.readUint; + + const entryCount = view.readUint(4); + const entries = view.readEntries(entryCount, () => ({ + sampleCount: view.readUint(4), + sampleOffset: read(4), + })); + + return { + version, + flags, + entryCount, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/dref.ts b/lib/src/iso/bmff/parsers/dref.ts new file mode 100644 index 00000000..50a39b54 --- /dev/null +++ b/lib/src/iso/bmff/parsers/dref.ts @@ -0,0 +1,39 @@ +import type { Box } from '../Box.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.7.2 Data Reference Box + * + * @group ISOBMFF + * + * @beta + */ +export type DataReferenceBox = FullBox & { + entryCount: number; + entries: Box[]; +}; + +/** + * Parse a DataReferenceBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed DataReferenceBox + * + * @group ISOBMFF + * + * @beta + */ +export function dref(view: IsoView): DataReferenceBox { + const { version, flags } = view.readFullBox(); + const entryCount = view.readUint(4); + const entries = view.readBoxes(entryCount); + + return { + version, + flags, + entryCount, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/elng.ts b/lib/src/iso/bmff/parsers/elng.ts new file mode 100644 index 00000000..352c19e0 --- /dev/null +++ b/lib/src/iso/bmff/parsers/elng.ts @@ -0,0 +1,31 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:202x - 8.4.6 Extended language tag + * + * @group ISOBMFF + * + * @beta + */ +export type ExtendedLanguageBox = FullBox & { + extendedLanguage: string; +} + +/** + * Parse a ExtendedLanguageBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed ExtendedLanguageBox + * + * @group ISOBMFF + * + * @beta + */ +export function elng(view: IsoView): ExtendedLanguageBox { + return { + ...view.readFullBox(), + extendedLanguage: view.readUtf8(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/elst.ts b/lib/src/iso/bmff/parsers/elst.ts new file mode 100644 index 00000000..096430c9 --- /dev/null +++ b/lib/src/iso/bmff/parsers/elst.ts @@ -0,0 +1,60 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * An edit list entry. + * + * @group ISOBMFF + * + * @beta + */ +export type EditListEntry = { + segmentDuration: number; + mediaTime: number; + mediaRateInteger: number; + mediaRateFraction: number; +} + +/** + * ISO/IEC 14496-12:2012 - 8.6.6 Edit List Box + * + * @group ISOBMFF + * + * @beta + */ +export type EditListBox = FullBox & { + entryCount: number; + entries: EditListEntry[]; +} + +/** + * Parse a Box from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed Box + * + * @group ISOBMFF + * + * @beta + */ +export function elst(view: IsoView): EditListBox { + const { version, flags } = view.readFullBox(); + const v1 = version === 1; + const size = v1 ? 8 : 4; + + const entryCount = view.readUint(4); + const entries = view.readEntries(entryCount, () => ({ + segmentDuration: view.readUint(size), + mediaTime: view.readInt(size), + mediaRateInteger: view.readInt(2), + mediaRateFraction: view.readInt(2), + })); + + return { + version, + flags, + entryCount, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/emsg.ts b/lib/src/iso/bmff/parsers/emsg.ts new file mode 100644 index 00000000..05ab3d70 --- /dev/null +++ b/lib/src/iso/bmff/parsers/emsg.ts @@ -0,0 +1,58 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 23009-1:2014 - 5.10.3.3 Event Message Box + * + * @group ISOBMFF + * + * @beta + */ +export type EventMessageBox = FullBox & { + schemeIdUri: string, + value: string, + timescale: number, + presentationTime: number, + presentationTimeDelta: number, + eventDuration: number, + id: number, + messageData: Uint8Array, +} + +/** + * Parse an EventMessageBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed EventMessageBox + * + * @group ISOBMFF + * + * @beta + */ +export function emsg(view: IsoView): EventMessageBox { + const { readUint, readString, readData } = view; + + const result = { ...view.readFullBox() } as EventMessageBox; + + if (result.version == 1) { + result.timescale = readUint(4); + result.presentationTime = readUint(8); + result.eventDuration = readUint(4); + result.id = readUint(4); + result.schemeIdUri = readString(-1); + result.value = readString(-1); + } + else { + result.schemeIdUri = readString(-1); + result.value = readString(-1); + result.timescale = readUint(4); + result.presentationTimeDelta = readUint(4); + result.eventDuration = readUint(4); + result.id = readUint(4); + } + + result.messageData = readData(-1); + + return result; +} diff --git a/lib/src/iso/bmff/parsers/enca.ts b/lib/src/iso/bmff/parsers/enca.ts new file mode 100644 index 00000000..3b589989 --- /dev/null +++ b/lib/src/iso/bmff/parsers/enca.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { mp4a, type AudioSampleEntry } from './mp4a.js'; + +/** + * Parse an AudioSampleEntry from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed AudioSampleEntry + * + * @group ISOBMFF + * + * @beta + */ +export const enca: BoxParser = mp4a; diff --git a/lib/src/iso/bmff/parsers/encv.ts b/lib/src/iso/bmff/parsers/encv.ts new file mode 100644 index 00000000..2beb705c --- /dev/null +++ b/lib/src/iso/bmff/parsers/encv.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export const encv: BoxParser = avc1; diff --git a/lib/src/iso/bmff/parsers/free.ts b/lib/src/iso/bmff/parsers/free.ts new file mode 100644 index 00000000..ddc3ce60 --- /dev/null +++ b/lib/src/iso/bmff/parsers/free.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.1.2 Free Space Box + * + * @group ISOBMFF + * + * @beta + */ +export type FreeSpaceBox = { + data: Uint8Array; +}; + +/** + * Parse a Box from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed Box + * + * @group ISOBMFF + * + * @beta + */ +export function free(view: IsoView): FreeSpaceBox { + return { + data: view.readData(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/frma.ts b/lib/src/iso/bmff/parsers/frma.ts new file mode 100644 index 00000000..e4033bf9 --- /dev/null +++ b/lib/src/iso/bmff/parsers/frma.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.12.2 Original Format Box + * + * @group ISOBMFF + * + * @beta + */ +export type OriginalFormatBox = { + dataFormat: number; +} + +/** + * Parse an OriginalFormatBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed OriginalFormatBox + * + * @group ISOBMFF + * + * @beta + */ +export function frma(view: IsoView): OriginalFormatBox { + return { + dataFormat: view.readUint(4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/ftyp.ts b/lib/src/iso/bmff/parsers/ftyp.ts new file mode 100644 index 00000000..d01355dd --- /dev/null +++ b/lib/src/iso/bmff/parsers/ftyp.ts @@ -0,0 +1,37 @@ +import type { IsoView } from '../IsoView.js'; +import type { TypeBox } from '../TypeBox.js'; +import { STRING } from '../fields/STRING.js'; + +/** + * ISO/IEC 14496-12:2012 - 4.3 File Type Box + * + * @group ISOBMFF + * + * @beta + */ +export type FileTypeBox = TypeBox; + +/** + * Parse a FileTypeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed FileTypeBox + * + * @group ISOBMFF + * + * @beta + */ +export function ftyp(view: IsoView): FileTypeBox { + const size = 4; + const majorBrand = view.readString(4); + const minorVersion = view.readUint(4); + const length = view.bytesRemaining / size; + const compatibleBrands = view.readArray(STRING, size, length); + + return { + majorBrand, + minorVersion, + compatibleBrands, + }; +} diff --git a/lib/src/iso/bmff/parsers/hdlr.ts b/lib/src/iso/bmff/parsers/hdlr.ts new file mode 100644 index 00000000..c9cb5333 --- /dev/null +++ b/lib/src/iso/bmff/parsers/hdlr.ts @@ -0,0 +1,37 @@ +import { UINT } from '../fields/UINT.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.4.3 Handler Reference Box + * + * @group ISOBMFF + * + * @beta + */ +export type HandlerReferenceBox = { + preDefined: number; + handlerType: string; + reserved: number[]; + name: string; +}; + +/** + * Parse a HandlerReferenceBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed HandlerReferenceBox + * + * @group ISOBMFF + * + * @beta + */ +export function hdlr(view: IsoView): HandlerReferenceBox { + return { + ...view.readFullBox(), + preDefined: view.readUint(4), + handlerType: view.readString(4), + reserved: view.readArray(UINT, 4, 3), + name: view.readString(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/hev1.ts b/lib/src/iso/bmff/parsers/hev1.ts new file mode 100644 index 00000000..35a48402 --- /dev/null +++ b/lib/src/iso/bmff/parsers/hev1.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export const hev1: BoxParser = avc1; diff --git a/lib/src/iso/bmff/parsers/hvc1.ts b/lib/src/iso/bmff/parsers/hvc1.ts new file mode 100644 index 00000000..96b1d4ac --- /dev/null +++ b/lib/src/iso/bmff/parsers/hvc1.ts @@ -0,0 +1,17 @@ +import type { IsoView } from '../IsoView.js'; +import { avc1, type VisualSampleEntry } from './avc1.js'; + +/** + * Parse a VisualSampleEntryBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VisualSampleEntryBox + * + * @group ISOBMFF + * + * @beta + */ +export function hvc1(view: IsoView): VisualSampleEntry { + return avc1(view); +} diff --git a/lib/src/iso/bmff/parsers/imda.ts b/lib/src/iso/bmff/parsers/imda.ts new file mode 100644 index 00000000..62b43872 --- /dev/null +++ b/lib/src/iso/bmff/parsers/imda.ts @@ -0,0 +1,31 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 9.1.4.1 Identified media data box + * + * @group ISOBMFF + * + * @beta + */ +export type IdentifiedMediaDataBox = { + imdaIdentifier: number; + data: Uint8Array; +}; + +/** + * Parse a IdentifiedMediaDataBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed IdentifiedMediaDataBox + * + * @group ISOBMFF + * + * @beta + */ +export function imda(view: IsoView): IdentifiedMediaDataBox { + return { + imdaIdentifier: view.readUint(4), + data: view.readData(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/kind.ts b/lib/src/iso/bmff/parsers/kind.ts new file mode 100644 index 00000000..f7085af5 --- /dev/null +++ b/lib/src/iso/bmff/parsers/kind.ts @@ -0,0 +1,33 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:202x - 8.10.4 Track kind box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackKindBox = FullBox & { + schemeUri: string; + value: string; +} + +/** + * Parse a TrackKinBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackKindBox + * + * @group ISOBMFF + * + * @beta + */ +export function kind(view: IsoView): TrackKindBox { + return { + ...view.readFullBox(), + schemeUri: view.readUtf8(-1), + value: view.readUtf8(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/labl.ts b/lib/src/iso/bmff/parsers/labl.ts new file mode 100644 index 00000000..b0c4a3bf --- /dev/null +++ b/lib/src/iso/bmff/parsers/labl.ts @@ -0,0 +1,40 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:202x - 8.10.5 Label box + * + * @group ISOBMFF + * + * @beta + */ +export type LabelBox = FullBox & { + isGroupLabel: boolean; + labelId: number; + language: string; + label: string; +} + +/** + * Parse a LabelBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed LabelBox + * + * @group ISOBMFF + * + * @beta + */ +export function labl(view: IsoView): LabelBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + isGroupLabel: (flags & 0x1) !== 0, + labelId: view.readUint(2), + language: view.readUtf8(-1), + label: view.readUtf8(-1), + }; +} diff --git a/lib/src/iso/bmff/parsers/mdat.ts b/lib/src/iso/bmff/parsers/mdat.ts new file mode 100644 index 00000000..d322be7b --- /dev/null +++ b/lib/src/iso/bmff/parsers/mdat.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.1.1 Media Data Box + * + * @group ISOBMFF + * + * @beta + */ +export type MediaDataBox = { + data: Uint8Array; +}; + +/** + * Parse a MediaDataBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MediaDataBox + * + * @group ISOBMFF + * + * @beta + */ +export function mdat(view: IsoView): MediaDataBox { + return { + data: view.readData(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/mdhd.ts b/lib/src/iso/bmff/parsers/mdhd.ts new file mode 100644 index 00000000..f3183147 --- /dev/null +++ b/lib/src/iso/bmff/parsers/mdhd.ts @@ -0,0 +1,66 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.4.2 Media Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type MediaHeaderBox = FullBox & { + /** A 32-bit integer that specifies the creation time of the media in this track. */ + creationTime: number; + + /** A 32-bit integer that specifies the most recent time the media in this track was modified. */ + modificationTime: number; + + /** A time value that indicates the time-scale for this media; this is the number of time units that pass in one second. */ + timescale: number; + + /** A time value that indicates the duration of this media. */ + duration: number; + + /** A 16-bit integer that specifies the language code for this media. */ + language: string; + + /** A 16-bit value that is reserved for use in other specifications. */ + preDefined: number; +} + +/** + * Parse a MediaHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MediaHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function mdhd(view: IsoView): MediaHeaderBox { + const { version, flags } = view.readFullBox(); + + const creationTime = view.readUint(version == 1 ? 8 : 4); + const modificationTime = view.readUint(version == 1 ? 8 : 4); + const timescale = view.readUint(4); + const duration = view.readUint(version == 1 ? 8 : 4); + const lang = view.readUint(2); + const language = String.fromCharCode(((lang >> 10) & 0x1F) + 0x60, + ((lang >> 5) & 0x1F) + 0x60, + (lang & 0x1F) + 0x60); + + const preDefined = view.readUint(2); + + return { + version, + flags, + creationTime, + modificationTime, + timescale, + duration, + language, + preDefined, + }; +} diff --git a/lib/src/iso/bmff/parsers/mehd.ts b/lib/src/iso/bmff/parsers/mehd.ts new file mode 100644 index 00000000..ddbcf215 --- /dev/null +++ b/lib/src/iso/bmff/parsers/mehd.ts @@ -0,0 +1,34 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.2 Movie Extends Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type MovieExtendsHeaderBox = FullBox & { + fragmentDuration: number; +}; + +/** + * Parse a MovieExtendsHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MovieExtendsHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function mehd(view: IsoView): MovieExtendsHeaderBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + fragmentDuration: view.readUint((version === 1) ? 8 : 4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/meta.ts b/lib/src/iso/bmff/parsers/meta.ts new file mode 100644 index 00000000..799196d9 --- /dev/null +++ b/lib/src/iso/bmff/parsers/meta.ts @@ -0,0 +1,26 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:202x - 8.11.1 Meta box + * + * @group ISOBMFF + * + * @beta + */ +export type MetaBox = FullBox; + +/** + * Parse a MetaBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MetaBox + * + * @group ISOBMFF + * + * @beta + */ +export function meta(view: IsoView): MetaBox { + return view.readFullBox(); +}; diff --git a/lib/src/iso/bmff/parsers/mfhd.ts b/lib/src/iso/bmff/parsers/mfhd.ts new file mode 100644 index 00000000..d33ca678 --- /dev/null +++ b/lib/src/iso/bmff/parsers/mfhd.ts @@ -0,0 +1,31 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.5 Movie Fragment Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type MovieFragmentHeaderBox = FullBox & { + sequenceNumber: number; +}; + +/** + * Parse a MovieFragmentHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MovieFragmentHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function mfhd(view: IsoView): MovieFragmentHeaderBox { + return { + ...view.readFullBox(), + sequenceNumber: view.readUint(4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/mfro.ts b/lib/src/iso/bmff/parsers/mfro.ts new file mode 100644 index 00000000..2a0cd737 --- /dev/null +++ b/lib/src/iso/bmff/parsers/mfro.ts @@ -0,0 +1,32 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.11 Movie Fragment Random Access Box + * + * @group ISOBMFF + * + * @beta + */ +export type MovieFragmentRandomAccessBox = FullBox & { + mfra_size: number; +} + +/** + * Parse a MovieFragmentRandomAccessBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed MovieFragmentRandomAccessBox + * + * @group ISOBMFF + * + * @beta + */ +export function mfro(view: IsoView): MovieFragmentRandomAccessBox { + return { + ...view.readFullBox(), + mfra_size: view.readUint(4), + }; +}; + diff --git a/lib/src/iso/bmff/parsers/mp4a.ts b/lib/src/iso/bmff/parsers/mp4a.ts new file mode 100644 index 00000000..14b8da7d --- /dev/null +++ b/lib/src/iso/bmff/parsers/mp4a.ts @@ -0,0 +1,47 @@ +import { UINT } from '../fields/UINT.js'; +import type { IsoView } from '../IsoView.js'; +import type { SampleEntry } from './avc1.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.5.2.2 mp4a box (use AudioSampleEntry definition and naming) + * + * @group ISOBMFF + * + * @beta + */ +export type AudioSampleEntry = SampleEntry & { + reserved2: number[]; + channelcount: number; + samplesize: number; + preDefined: number; + reserved3: number; + samplerate: number; + esds: Uint8Array; +}; + +/** + * Parse an AudioSampleEntry from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed AudioSampleEntry + * + * @group ISOBMFF + * + * @beta + */ +export function mp4a(view: IsoView): AudioSampleEntry { + const { readArray, readUint, readTemplate, readData } = view; + + return { + reserved1: readArray(UINT, 1, 6), + dataReferenceIndex: readUint(2), + reserved2: readArray(UINT, 4, 2), + channelcount: readUint(2), + samplesize: readUint(2), + preDefined: readUint(2), + reserved3: readUint(2), + samplerate: readTemplate(4), + esds: readData(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/mvhd.ts b/lib/src/iso/bmff/parsers/mvhd.ts new file mode 100644 index 00000000..239f9f13 --- /dev/null +++ b/lib/src/iso/bmff/parsers/mvhd.ts @@ -0,0 +1,59 @@ +import type { IsoView } from '../IsoView.js'; +import { UINT } from '../fields/UINT.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.2.2 Movie Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type MovieHeaderBox = { + version: number; + flags: number; + creationTime: number; + modificationTime: number; + timescale: number; + duration: number; + rate: number; + volume: number; + reserved1: number; + reserved2: number[]; + matrix: number[]; + preDefined: number[]; + nextTrackId: number; +}; + +/** + * Parse a Box from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed Box + * + * @group ISOBMFF + * + * @beta + */ +export function mvhd(view: IsoView): MovieHeaderBox { + const { readUint, readTemplate, readArray } = view; + + const { version, flags } = view.readFullBox(); + const size = (version == 1) ? 8 : 4; + + return { + version, + flags, + creationTime: readUint(size), + modificationTime: readUint(size), + timescale: readUint(4), + duration: readUint(size), + rate: readTemplate(4), + volume: readTemplate(2), + reserved1: readUint(2), + reserved2: readArray(UINT, 4, 2), + matrix: readArray(UINT, 4, 9), + preDefined: readArray(UINT, 4, 6), + nextTrackId: readUint(4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/payl.ts b/lib/src/iso/bmff/parsers/payl.ts new file mode 100644 index 00000000..90cb7606 --- /dev/null +++ b/lib/src/iso/bmff/parsers/payl.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-30:2014 - WebVTT Cue Payload Box. + * + * @group ISOBMFF + * + * @beta + */ +export type WebVTTCuePayloadBox = { + cueText: string; +}; + +/** + * Parse a WebVTTCuePayloadBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed WebVTTCuePayloadBox + * + * @group ISOBMFF + * + * @beta + */ +export function payl(view: IsoView): WebVTTCuePayloadBox { + return { + cueText: view.readUtf8(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/prft.ts b/lib/src/iso/bmff/parsers/prft.ts new file mode 100644 index 00000000..c21fadbf --- /dev/null +++ b/lib/src/iso/bmff/parsers/prft.ts @@ -0,0 +1,40 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.16.5 Producer Reference Time + * + * @group ISOBMFF + * + * @beta + */ +export type ProducerReferenceTimeBox = FullBox & { + referenceTrackId: number; + ntpTimestampSec: number; + ntpTimestampFrac: number; + mediaTime: number; +}; + +/** + * Parse a ProducerReferenceTimeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed ProducerReferenceTimeBox + * + * @group ISOBMFF + * + * @beta + */ +export function prft(view: IsoView): ProducerReferenceTimeBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + referenceTrackId: view.readUint(4), + ntpTimestampSec: view.readUint(4), + ntpTimestampFrac: view.readUint(4), + mediaTime: view.readUint(version === 1 ? 8 : 4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/prsl.ts b/lib/src/iso/bmff/parsers/prsl.ts new file mode 100644 index 00000000..ef2abc16 --- /dev/null +++ b/lib/src/iso/bmff/parsers/prsl.ts @@ -0,0 +1,70 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * Entity + * + * @group ISOBMFF + * + * @beta + */ +export type Entity = { + /** Entity ID */ + entityId: number; +}; + +/** + * ISO/IEC 14496-12:202x - 8.18.4.1 Preselection group box + * + * @group ISOBMFF + * + * @beta + */ +export type PreselectionGroupBox = FullBox & { + /** Group ID */ + groupId: number; + + /** Number of entities in group */ + numEntitiesInGroup: number; + + /** Entities */ + entities: Entity[]; + + preselectionTag?: string; + selectionPriority?: number; + interleavingTag?: string; +}; + +/** + * Parse a PreselectionGroupBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed PreselectionGroupBox + * + * @group ISOBMFF + * + * @beta + */ +export function prsl(view: IsoView): PreselectionGroupBox { + const { version, flags } = view.readFullBox(); + const groupId = view.readUint(4); + const numEntitiesInGroup = view.readUint(4); + const entities = view.readEntries(numEntitiesInGroup, () => ({ + entityId: view.readUint(4), + })); + const preselectionTag = flags & 0x1000 ? view.readUtf8(-1) : undefined; + const selectionPriority = flags & 0x2000 ? view.readUint(1) : undefined; + const interleavingTag = flags & 0x4000 ? view.readUtf8(-1) : undefined; + + return { + version, + flags, + groupId, + numEntitiesInGroup, + entities, + preselectionTag, + selectionPriority, + interleavingTag, + }; +} diff --git a/lib/src/iso/bmff/parsers/pssh.ts b/lib/src/iso/bmff/parsers/pssh.ts new file mode 100644 index 00000000..8e2e5aec --- /dev/null +++ b/lib/src/iso/bmff/parsers/pssh.ts @@ -0,0 +1,44 @@ +import { UINT } from '../fields/UINT.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 23001-7:2011 - 8.1 Protection System Specific Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type ProtectionSystemSpecificHeaderBox = FullBox & { + systemID: number[]; + dataSize: number; + data: number[]; +} + +/** + * Parse a ProtectionSystemSpecificHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed ProtectionSystemSpecificHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function pssh(view: IsoView): ProtectionSystemSpecificHeaderBox { + const { readUint, readArray } = view; + const { version, flags } = view.readFullBox(); + + const systemID = readArray(UINT, 1, 16); + const dataSize = readUint(4); + const data = readArray(UINT, 1, dataSize); + + return { + version, + flags, + systemID, + dataSize, + data, + }; +}; diff --git a/lib/src/iso/bmff/parsers/schm.ts b/lib/src/iso/bmff/parsers/schm.ts new file mode 100644 index 00000000..a5cc5bd4 --- /dev/null +++ b/lib/src/iso/bmff/parsers/schm.ts @@ -0,0 +1,38 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.12.5 Scheme Type Box + * + * @group ISOBMFF + * + * @beta + */ +export type SchemeTypeBox = FullBox & { + schemeType: number; + schemeVersion: number; + schemeUri?: string; +}; + +/** + * Parse a SchemeTypeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SchemeTypeBox + * + * @group ISOBMFF + * + * @beta + */ +export function schm(view: IsoView): SchemeTypeBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + schemeType: view.readUint(4), + schemeVersion: view.readUint(4), + schemeUri: flags & 0x000001 ? view.readString(-1) : undefined, + }; +}; diff --git a/lib/src/iso/bmff/parsers/sdtp.ts b/lib/src/iso/bmff/parsers/sdtp.ts new file mode 100644 index 00000000..5cc65114 --- /dev/null +++ b/lib/src/iso/bmff/parsers/sdtp.ts @@ -0,0 +1,34 @@ +import { UINT } from '../fields/UINT.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.6.4.1 Sample Dependency Type box + * + * @group ISOBMFF + * + * @beta + */ +export type SampleDependencyTypeBox = FullBox & { + sampleDependencyTable: number[]; +}; + +// + +/** + * Parse a SampleDependencyTypeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SampleDependencyTypeBox + * + * @group ISOBMFF + * + * @beta + */ +export function sdtp(view: IsoView): SampleDependencyTypeBox { + return { + ...view.readFullBox(), + sampleDependencyTable: view.readArray(UINT, 1, view.bytesRemaining), + }; +}; diff --git a/lib/src/iso/bmff/parsers/sidx.ts b/lib/src/iso/bmff/parsers/sidx.ts new file mode 100644 index 00000000..03900027 --- /dev/null +++ b/lib/src/iso/bmff/parsers/sidx.ts @@ -0,0 +1,86 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * Segment index reference + * + * @group ISOBMFF + * + * @beta + */ +export type Reference = { + reference: number, + subsegmentDuration: number, + sap: number, + referenceType: number, + referencedSize: number, + startsWithSap: number, + sapType: number, + sapDeltaTime: number, +} + +/** + * ISO/IEC 14496-12:2012 - 8.16.3 Segment Index Box + * + * @group ISOBMFF + * + * @beta + */ +export type SegmentIndexBox = FullBox & { + referenceId: number, + timescale: number, + earliestPresentationTime: number, + firstOffset: number, + reserved: number, + references: Reference[], +}; + +/** + * Parse a SegmentIndexBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SegmentIndexBox + * + * @group ISOBMFF + * + * @beta + */ +export function sidx(view: IsoView): SegmentIndexBox { + const { readUint } = view; + const { version, flags } = view.readFullBox(); + const v1 = version === 1; + const size = v1 ? 8 : 4; + + const referenceId = readUint(4); + const timescale = readUint(4); + const earliestPresentationTime = readUint(size); + const firstOffset = readUint(size); + const reserved = readUint(2); + const referenceCount = readUint(2); + const references = view.readEntries(referenceCount, () => { + const entry = {} as any; + + entry.reference = readUint(4); + entry.subsegmentDuration = readUint(4); + entry.sap = readUint(4); + entry.referenceType = (entry.reference >> 31) & 0x00000001; + entry.referencedSize = entry.reference & 0x7FFFFFFF; + entry.startsWithSap = (entry.sap >> 31) & 0x00000001; + entry.sapType = (entry.sap >> 28) & 0x00000007; + entry.sapDeltaTime = (entry.sap & 0x0FFFFFFF); + + return entry; + }); + + return { + version, + flags, + referenceId, + timescale, + earliestPresentationTime, + firstOffset, + reserved, + references, + }; +}; diff --git a/lib/src/iso/bmff/parsers/skip.ts b/lib/src/iso/bmff/parsers/skip.ts new file mode 100644 index 00000000..c315b471 --- /dev/null +++ b/lib/src/iso/bmff/parsers/skip.ts @@ -0,0 +1,15 @@ +import type { BoxParser } from '../BoxParser.js'; +import { free, type FreeSpaceBox } from './free.js'; + +/** + * Parse a FreeSpaceBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed FreeSpaceBox + * + * @group ISOBMFF + * + * @beta + */ +export const skip: BoxParser = free; diff --git a/lib/src/iso/bmff/parsers/smhd.ts b/lib/src/iso/bmff/parsers/smhd.ts new file mode 100644 index 00000000..853882d1 --- /dev/null +++ b/lib/src/iso/bmff/parsers/smhd.ts @@ -0,0 +1,33 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.4.5.3 Sound Media Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type SoundMediaHeaderBox = FullBox & { + balance: number; + reserved: number; +}; + +/** + * Parse a SoundMediaHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SoundMediaHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function smhd(view: IsoView): SoundMediaHeaderBox { + return { + ...view.readFullBox(), + balance: view.readUint(2), + reserved: view.readUint(2), + }; +}; diff --git a/lib/src/iso/bmff/parsers/ssix.ts b/lib/src/iso/bmff/parsers/ssix.ts new file mode 100644 index 00000000..2e073d11 --- /dev/null +++ b/lib/src/iso/bmff/parsers/ssix.ts @@ -0,0 +1,69 @@ +import type { FullBox } from '../FullBox'; +import type { IsoView } from '../IsoView'; + +/** + * Subsegment range + * + * @group ISOBMFF + * + * @beta + */ +export type Range = { + level: number; + rangeSize: number; +}; + +/** + * Subsegment + * + * @group ISOBMFF + * + * @beta + */ +export type Subsegment = { + rangesCount: number; + ranges: Range[]; +}; + +/** + * ISO/IEC 14496-12:2012 - 8.16.4 Subsegment Index Box + * + * @group ISOBMFF + * + * @beta + */ +export type SubsegmentIndexBox = FullBox & { + subsegmentCount: number; + subsegments: Subsegment[]; +}; + +/** + * Parse a SubsegmentIndexBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SubsegmentIndexBox + * + * @group ISOBMFF + * + * @beta + */ +export function ssix(view: IsoView): SubsegmentIndexBox { + const { version, flags } = view.readFullBox(); + const subsegmentCount = view.readUint(4); + const subsegments = view.readEntries(subsegmentCount, () => { + const rangesCount = view.readUint(4); + const ranges = view.readEntries(rangesCount, () => ({ + level: view.readUint(1), + rangeSize: view.readUint(3), + })); + return { rangesCount, ranges }; + }); + + return { + version, + flags, + subsegmentCount, + subsegments, + }; +}; diff --git a/lib/src/iso/bmff/parsers/sthd.ts b/lib/src/iso/bmff/parsers/sthd.ts new file mode 100644 index 00000000..10753c65 --- /dev/null +++ b/lib/src/iso/bmff/parsers/sthd.ts @@ -0,0 +1,26 @@ +import type { FullBox } from '../FullBox'; +import type { IsoView } from '../IsoView'; + +/** + * ISO/IEC 14496-12:2015 - 12.6.2 Subtitle media header Box + * + * @group ISOBMFF + * + * @beta + */ +export type SubtitleMediaHeaderBox = FullBox; + +/** + * Parse a SubtitleMediaHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SubtitleMediaHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function sthd(view: IsoView): SubtitleMediaHeaderBox { + return view.readFullBox(); +}; diff --git a/lib/src/iso/bmff/parsers/stsd.ts b/lib/src/iso/bmff/parsers/stsd.ts new file mode 100644 index 00000000..35fdd1db --- /dev/null +++ b/lib/src/iso/bmff/parsers/stsd.ts @@ -0,0 +1,37 @@ +import type { FullBox } from '../FullBox'; +import type { IsoView } from '../IsoView'; + +/** + * ISO/IEC 14496-12:2012 - 8.5.2 Sample Description Box + * + * @group ISOBMFF + * + * @beta + */ +export type SampleDescriptionBox = FullBox & { + entryCount: number, + entries: any[], +}; + +/** + * Parse a SampleDescriptionBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SampleDescriptionBox + * + * @group ISOBMFF + * + * @beta + */ +export function stsd(view: IsoView): SampleDescriptionBox { + const { version, flags } = view.readFullBox(); + const entryCount = view.readUint(4); + + return { + version, + flags, + entryCount, + entries: view.readBoxes(entryCount), + }; +}; diff --git a/lib/src/iso/bmff/parsers/stss.ts b/lib/src/iso/bmff/parsers/stss.ts new file mode 100644 index 00000000..cf802a9a --- /dev/null +++ b/lib/src/iso/bmff/parsers/stss.ts @@ -0,0 +1,50 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * Sync sample + * + * @group ISOBMFF + * + * @beta + */ +export type SyncSample = { + sampleNumber: number; +} + +/** + * ISO/IEC 14496-12:2015 - 8.6.2 Sync Sample Box + * + * @group ISOBMFF + * + * @beta + */ +export type SyncSampleBox = FullBox & { + entryCount: number; + entries: SyncSample[]; +}; + +/** + * Parse a SyncSampleBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SyncSampleBox + * + * @group ISOBMFF + * + * @beta + */ +export function stss(view: IsoView): SyncSampleBox { + const { version, flags } = view.readFullBox(); + const entryCount = view.readUint(4); + + return { + version, + flags, + entryCount, + entries: view.readEntries(entryCount, () => ({ + sampleNumber: view.readUint(4), + })), + }; +}; diff --git a/lib/src/iso/bmff/parsers/sttg.ts b/lib/src/iso/bmff/parsers/sttg.ts new file mode 100644 index 00000000..95888230 --- /dev/null +++ b/lib/src/iso/bmff/parsers/sttg.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-30:2014 - WebVTT Cue Settings Box. + * + * @group ISOBMFF + * + * @beta + */ +export type WebVTTSettingsBox = { + settings: string; +}; + +/** + * Parse a WebVTTSettingsBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed WebVTTSettingsBox + * + * @group ISOBMFF + * + * @beta + */ +export function sttg(view: IsoView): WebVTTSettingsBox { + return { + settings: view.readUtf8(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/stts.ts b/lib/src/iso/bmff/parsers/stts.ts new file mode 100644 index 00000000..32a1e345 --- /dev/null +++ b/lib/src/iso/bmff/parsers/stts.ts @@ -0,0 +1,59 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * sample + * + * @group ISOBMFF + * + * @beta + */ +export type DecodingTimeSample = { + /** A 32-bit integer that specifies the number of consecutive samples that have the same decoding time delta. */ + sampleCount: number; + + /** A 32-bit integer that specifies the delta of the decoding time of each sample in the table. */ + sampleDelta: number; +}; + +/** + * ISO/IEC 14496-12:2012 - 8.6.1.2 Decoding Time To Sample Box + * + * @group ISOBMFF + * + * @beta + */ +export type DecodingTimeToSampleBox = FullBox & { + /** A 32-bit integer that specifies the number of entries in the decoding time-to-sample table. */ + entryCount: number; + + /** An array of decoding time-to-sample entries. */ + entries: DecodingTimeSample[]; +}; + +/** + * Parse a DecodingTimeToSampleBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed DecodingTimeToSampleBox + * + * @group ISOBMFF + * + * @beta + */ +export function stts(view: IsoView): DecodingTimeToSampleBox { + const { version, flags } = view.readFullBox(); + const entryCount = view.readUint(4); + const entries = view.readEntries(entryCount, () => ({ + sampleCount: view.readUint(4), + sampleDelta: view.readUint(4), + })); + + return { + version, + flags, + entryCount, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/styp.ts b/lib/src/iso/bmff/parsers/styp.ts new file mode 100644 index 00000000..f391feda --- /dev/null +++ b/lib/src/iso/bmff/parsers/styp.ts @@ -0,0 +1,25 @@ +import type { BoxParser } from '../BoxParser.js'; +import { type TypeBox } from '../TypeBox.js'; +import { ftyp } from './ftyp.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.16.2 Segment Type Box + * + * @group ISOBMFF + * + * @beta + */ +export type SegmentTypeBox = TypeBox; + +/** + * Parse a SegmentTypeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SegmentTypeBox + * + * @group ISOBMFF + * + * @beta + */ +export const styp: BoxParser = ftyp; diff --git a/lib/src/iso/bmff/parsers/subs.ts b/lib/src/iso/bmff/parsers/subs.ts new file mode 100644 index 00000000..33df2479 --- /dev/null +++ b/lib/src/iso/bmff/parsers/subs.ts @@ -0,0 +1,80 @@ +import type { FullBox } from '../FullBox'; +import type { IsoView } from '../IsoView'; + +/** + * Sub sample + * + * @group ISOBMFF + * + * @beta + */ +export type SubSample = { + subsampleSize: number; + subsamplePriority: number; + discardable: number; + codecSpecificParameters: number; +} + +/** + * Sub sample entry + * + * @group ISOBMFF + * + * @beta + */ +export type SubSampleEntry = { + sampleDelta: number; + subsampleCount: number; + subsamples: SubSample[]; +}; + +/** + * ISO/IEC 14496-12:2015 - 8.7.7 Sub-Sample Information Box + * + * @group ISOBMFF + * + * @beta + */ +export type SubSampleInformationBox = FullBox & { + entryCount: number; + entries: SubSampleEntry[]; +}; + +/** + * Parse a SubSampleInformationBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed SubSampleInformationBox + * + * @group ISOBMFF + * + * @beta + */ +export function subs(view: IsoView): SubSampleInformationBox { + const { version, flags } = view.readFullBox(); + const entryCount = view.readUint(4); + const entries = view.readEntries(entryCount, () => { + const sampleDelta = view.readUint(4); + const subsampleCount = view.readUint(2); + const subsamples = view.readEntries(subsampleCount, () => ({ + subsampleSize: view.readUint((version === 1) ? 4 : 2), + subsamplePriority: view.readUint(1), + discardable: view.readUint(1), + codecSpecificParameters: view.readUint(4), + })); + + return { + sampleDelta, + subsampleCount, + subsamples, + }; + }); + + return { + version, + flags, + entryCount, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/tenc.ts b/lib/src/iso/bmff/parsers/tenc.ts new file mode 100644 index 00000000..ec4d057c --- /dev/null +++ b/lib/src/iso/bmff/parsers/tenc.ts @@ -0,0 +1,36 @@ +import { UINT } from '../fields/UINT.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 23001-7:2011 - 8.2 Track Encryption Box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackEncryptionBox = FullBox & { + defaultIsEncrypted: number; + defaultIvSize: number; + defaultKid: number[]; +}; + +/** + * Parse a TrackEncryptionBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackEncryptionBox + * + * @group ISOBMFF + * + * @beta + */ +export function tenc(view: IsoView): TrackEncryptionBox { + return { + ...view.readFullBox(), + defaultIsEncrypted: view.readUint(3), + defaultIvSize: view.readUint(1), + defaultKid: view.readArray(UINT, 1, 16), + }; +}; diff --git a/lib/src/iso/bmff/parsers/tfdt.ts b/lib/src/iso/bmff/parsers/tfdt.ts new file mode 100644 index 00000000..9191d6ac --- /dev/null +++ b/lib/src/iso/bmff/parsers/tfdt.ts @@ -0,0 +1,34 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.12 Track Fragment Decode Time + * + * @group ISOBMFF + * + * @beta + */ +export type TrackFragmentDecodeTimeBox = FullBox & { + baseMediaDecodeTime: number; +}; + +/** + * Parse a TrackFragmentDecodeTimeBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackFragmentDecodeTimeBox + * + * @group ISOBMFF + * + * @beta + */ +export function tfdt(view: IsoView): TrackFragmentDecodeTimeBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + baseMediaDecodeTime: view.readUint((version == 1) ? 8 : 4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/tfhd.ts b/lib/src/iso/bmff/parsers/tfhd.ts new file mode 100644 index 00000000..5bc56bf7 --- /dev/null +++ b/lib/src/iso/bmff/parsers/tfhd.ts @@ -0,0 +1,44 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.7 Track Fragment Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackFragmentHeaderBox = FullBox & { + trackId: number; + baseDataOffset?: number; + sampleDescriptionOffset?: number; + defaultSampleDuration?: number; + defaultSampleSize?: number; + defaultSampleFlags?: number; +}; + +/** + * Parse a TrackFragmentHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackFragmentHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function tfhd(view: IsoView): TrackFragmentHeaderBox { + const { version, flags } = view.readFullBox(); + + return { + version, + flags, + trackId: view.readUint(4), + baseDataOffset: flags & 0x01 ? view.readUint(8) : undefined, + sampleDescriptionOffset: flags & 0x02 ? view.readUint(4) : undefined, + defaultSampleDuration: flags & 0x08 ? view.readUint(4) : undefined, + defaultSampleSize: flags & 0x10 ? view.readUint(4) : undefined, + defaultSampleFlags: flags & 0x20 ? view.readUint(4) : undefined, + }; +}; diff --git a/lib/src/iso/bmff/parsers/tfra.ts b/lib/src/iso/bmff/parsers/tfra.ts new file mode 100644 index 00000000..965d40f9 --- /dev/null +++ b/lib/src/iso/bmff/parsers/tfra.ts @@ -0,0 +1,77 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView'; + +/** + * Track fragment random access entry + * + * @group ISOBMFF + * + * @beta + */ +export type TrackFragmentRandomAccessEntry = { + time: number; + moofOffset: number; + trafNumber: number; + trunNumber: number; + sampleNumber: number; +} + +/** + * ISO/IEC 14496-12:2012 - 8.8.10 Track Fragment Random Access Box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackFragmentRandomAccessBox = FullBox & { + trackId: number; + reserved: number; + numberOfEntry: number; + lengthSizeOfTrafNum: number; + lengthSizeOfTrunNum: number; + lengthSizeOfSampleNum: number; + entries: TrackFragmentRandomAccessEntry[]; +}; + +/** + * Parse a TrackFragmentRandomAccessBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackFragmentRandomAccessBox + * + * @group ISOBMFF + * + * @beta + */ +export function tfra(view: IsoView): TrackFragmentRandomAccessBox { + const { version, flags } = view.readFullBox(); + const trackId = view.readUint(4); + const reserved = view.readUint(4); + + const lengthSizeOfTrafNum = (reserved & 0x00000030) >> 4; + const lengthSizeOfTrunNum = (reserved & 0x0000000C) >> 2; + const lengthSizeOfSampleNum = (reserved & 0x00000003); + + const numberOfEntry = view.readUint(4); + + const entries = view.readEntries(numberOfEntry, () => ({ + time: view.readUint((version === 1) ? 8 : 4), + moofOffset: view.readUint((version === 1) ? 8 : 4), + trafNumber: view.readUint(lengthSizeOfTrafNum + 1), + trunNumber: view.readUint(lengthSizeOfTrunNum + 1), + sampleNumber: view.readUint(lengthSizeOfSampleNum + 1), + })); + + return { + version, + flags, + trackId, + reserved, + lengthSizeOfTrafNum, + lengthSizeOfTrunNum, + lengthSizeOfSampleNum, + numberOfEntry, + entries, + }; +}; diff --git a/lib/src/iso/bmff/parsers/tkhd.ts b/lib/src/iso/bmff/parsers/tkhd.ts new file mode 100644 index 00000000..21b0b03a --- /dev/null +++ b/lib/src/iso/bmff/parsers/tkhd.ts @@ -0,0 +1,61 @@ +import { TEMPLATE } from '../fields/TEMPLATE.js'; +import { UINT } from '../fields/UINT.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.3.2 Track Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackHeaderBox = FullBox & { + creationTime: number; + modificationTime: number; + trackId: number; + reserved1: number; + duration: number; + reserved2: number[]; + layer: number; + alternateGroup: number; + volume: number; + reserved3: number; + matrix: number[]; + width: number; + height: number; +}; + +/** + * Parse a TrackHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function tkhd(view: IsoView): TrackHeaderBox { + const { version, flags } = view.readFullBox(); + const size = version === 1 ? 8 : 4; + + return { + version, + flags, + creationTime: view.readUint(size), + modificationTime: view.readUint(size), + trackId: view.readUint(4), + reserved1: view.readUint(4), + duration: view.readUint(size), + reserved2: view.readArray(UINT, 4, 2), + layer: view.readUint(2), + alternateGroup: view.readUint(2), + volume: view.readTemplate(2), + reserved3: view.readUint(2), + matrix: view.readArray(TEMPLATE, 4, 9), + width: view.readTemplate(4), + height: view.readTemplate(4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/trex.ts b/lib/src/iso/bmff/parsers/trex.ts new file mode 100644 index 00000000..b8fd6016 --- /dev/null +++ b/lib/src/iso/bmff/parsers/trex.ts @@ -0,0 +1,39 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.8.3 Track Extends Box + * + * @group ISOBMFF + * + * @beta + */ +export type TrackExtendsBox = FullBox & { + trackId: number; + defaultSampleDescriptionIndex: number; + defaultSampleDuration: number; + defaultSampleSize: number; + defaultSampleFlags: number; +}; + +/** + * Parse a TrackExtendsBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackExtendsBox + * + * @group ISOBMFF + * + * @beta + */ +export function trex(view: IsoView): TrackExtendsBox { + return { + ...view.readFullBox(), + trackId: view.readUint(4), + defaultSampleDescriptionIndex: view.readUint(4), + defaultSampleDuration: view.readUint(4), + defaultSampleSize: view.readUint(4), + defaultSampleFlags: view.readUint(4), + }; +}; diff --git a/lib/src/iso/bmff/parsers/trun.ts b/lib/src/iso/bmff/parsers/trun.ts new file mode 100644 index 00000000..b54e3f36 --- /dev/null +++ b/lib/src/iso/bmff/parsers/trun.ts @@ -0,0 +1,87 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * Track run sample + * + * @group ISOBMFF + * + * @beta + */ +export type TrackRunSample = { + sampleDuration?: number; + sampleSize?: number; + sampleFlags?: number; + sampleCompositionTimeOffset?: number; +} + +/** + * ISO/IEC 14496-12:2012 - 8.8.8 Track Run Box + * + * Note: the 'trun' box has a direct relation to the 'tfhd' box for defaults. + * These defaults are not set explicitly here, but are left to resolve for the user. + * + * @group ISOBMFF + * + * @beta + */ +export type TrackRunBox = FullBox & { + sampleCount: number; + dataOffset?: number; + firstSampleFlags?: number; + samples: TrackRunSample[]; +}; + +/** + * Parse a TrackRunBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed TrackRunBox + * + * @group ISOBMFF + * + * @beta + */ +export function trun(view: IsoView): TrackRunBox { + const { version, flags } = view.readFullBox(); + const sampleCount = view.readUint(4); + let dataOffset: number | undefined; + let firstSampleFlags: number | undefined; + + if (flags & 0x1) { + dataOffset = view.readInt(4); + } + + if (flags & 0x4) { + firstSampleFlags = view.readUint(4); + } + + const samples = view.readEntries(sampleCount, () => { + const sample: TrackRunSample = {}; + + if (flags & 0x100) { + sample.sampleDuration = view.readUint(4); + } + if (flags & 0x200) { + sample.sampleSize = view.readUint(4); + } + if (flags & 0x400) { + sample.sampleFlags = view.readUint(4); + } + if (flags & 0x800) { + sample.sampleCompositionTimeOffset = (version === 1) ? view.readInt(4) : view.readUint(4); + } + + return sample; + }); + + return { + version, + flags, + sampleCount, + dataOffset, + firstSampleFlags, + samples, + }; +}; diff --git a/lib/src/iso/bmff/parsers/url.ts b/lib/src/iso/bmff/parsers/url.ts new file mode 100644 index 00000000..ad6a93b1 --- /dev/null +++ b/lib/src/iso/bmff/parsers/url.ts @@ -0,0 +1,31 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.7.2 Data Reference Box + * + * @group ISOBMFF + * + * @beta + */ +export type UrlBox = FullBox & { + location: string; +}; + +/** + * Parse a UrlBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed UrlBox + * + * @group ISOBMFF + * + * @beta + */ +export function url(view: IsoView): UrlBox { + return { + ...view.readFullBox(), + location: view.readString(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/urn.ts b/lib/src/iso/bmff/parsers/urn.ts new file mode 100644 index 00000000..95a5eeaf --- /dev/null +++ b/lib/src/iso/bmff/parsers/urn.ts @@ -0,0 +1,33 @@ +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.7.2 Data Reference Box + * + * @group ISOBMFF + * + * @beta + */ +export type UrnBox = FullBox & { + name: string; + location: string; +}; + +/** + * Parse a UrnBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed UrnBox + * + * @group ISOBMFF + * + * @beta + */ +export function urn(view: IsoView): UrnBox { + return { + ...view.readFullBox(), + name: view.readString(-1), + location: view.readString(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/vlab.ts b/lib/src/iso/bmff/parsers/vlab.ts new file mode 100644 index 00000000..ce2b7fde --- /dev/null +++ b/lib/src/iso/bmff/parsers/vlab.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-30:2014 - WebVTT Source Label Box + * + * @group ISOBMFF + * + * @beta + */ +export type WebVTTSourceLabelBox = { + sourceLabel: string; +}; + +/** + * Parse a WebVTTSourceLabelBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed WebVTTSourceLabelBox + * + * @group ISOBMFF + * + * @beta + */ +export function vlab(view: IsoView): WebVTTSourceLabelBox { + return { + sourceLabel: view.readUtf8(-1), + }; +}; diff --git a/lib/src/iso/bmff/parsers/vmhd.ts b/lib/src/iso/bmff/parsers/vmhd.ts new file mode 100644 index 00000000..e6fd3f7d --- /dev/null +++ b/lib/src/iso/bmff/parsers/vmhd.ts @@ -0,0 +1,34 @@ +import { UINT } from '../fields/UINT.js'; +import type { FullBox } from '../FullBox.js'; +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-12:2012 - 8.4.5.2 Video Media Header Box + * + * @group ISOBMFF + * + * @beta + */ +export type VideoMediaHeaderBox = FullBox & { + graphicsmode: number, + opcolor: number[], +}; + +/** + * Parse a VideoMediaHeaderBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed VideoMediaHeaderBox + * + * @group ISOBMFF + * + * @beta + */ +export function vmhd(view: IsoView): VideoMediaHeaderBox { + return { + ...view.readFullBox(), + graphicsmode: view.readUint(2), + opcolor: view.readArray(UINT, 2, 3), + }; +}; diff --git a/lib/src/iso/bmff/parsers/vttC.ts b/lib/src/iso/bmff/parsers/vttC.ts new file mode 100644 index 00000000..d4e1e309 --- /dev/null +++ b/lib/src/iso/bmff/parsers/vttC.ts @@ -0,0 +1,29 @@ +import type { IsoView } from '../IsoView.js'; + +/** + * ISO/IEC 14496-30:2014 - WebVTT Configuration Box + * + * @group ISOBMFF + * + * @beta + */ +export type WebVTTConfigurationBox = { + config: string; +}; + +/** + * Parse a WebVTTConfigurationBox from an IsoView + * + * @param view - The IsoView to read data from + * + * @returns A parsed WebVTTConfigurationBox + * + * @group ISOBMFF + * + * @beta + */ +export function vttC(view: IsoView): WebVTTConfigurationBox { + return { + config: view.readUtf8(), + }; +}; diff --git a/lib/src/iso/bmff/parsers/vtte.ts b/lib/src/iso/bmff/parsers/vtte.ts new file mode 100644 index 00000000..c1243a30 --- /dev/null +++ b/lib/src/iso/bmff/parsers/vtte.ts @@ -0,0 +1,22 @@ +/** + * ISO/IEC 14496-30:2014 - WebVTT Empty Sample Box + * + * @group ISOBMFF + * + * @beta + */ +export type WebVTTEmptySampleBox = object; + +/** + * Parse a WebVTT Empty Sample Box from an IsoView + * + * @returns A parsed WebVTT Empty Sample Box + * + * @group ISOBMFF + * + * @beta + */ +export function vtte(): WebVTTEmptySampleBox { + // Nothing should happen here. + return {}; +}; diff --git a/lib/src/iso/bmff/readers/ISOFieldType.ts b/lib/src/iso/bmff/readers/ISOFieldType.ts new file mode 100644 index 00000000..b54522ad --- /dev/null +++ b/lib/src/iso/bmff/readers/ISOFieldType.ts @@ -0,0 +1 @@ +export type ISOFieldType = 'uint' | 'int' | 'template' | 'string' | 'data' | 'utf8'; diff --git a/lib/src/iso/bmff/readers/ISOFieldTypeMap.ts b/lib/src/iso/bmff/readers/ISOFieldTypeMap.ts new file mode 100644 index 00000000..25f03e07 --- /dev/null +++ b/lib/src/iso/bmff/readers/ISOFieldTypeMap.ts @@ -0,0 +1,16 @@ +/** + * ISOFieldTypeMap is a map of ISO BMFF field types to their corresponding JavaScript types. + * + * @group ISOBMFF + * + * @beta + */ +export type ISOFieldTypeMap = { + uint: number; + int: number; + template: number; + string: string; + data: Uint8Array; + utf8: string; + utf8string: string; +}; diff --git a/lib/src/iso/bmff/readers/dataViewToString.ts b/lib/src/iso/bmff/readers/dataViewToString.ts new file mode 100644 index 00000000..8f10d69c --- /dev/null +++ b/lib/src/iso/bmff/readers/dataViewToString.ts @@ -0,0 +1,49 @@ +export function dataViewToString(dataView: DataView, encoding: string = 'utf-8'): string { + if (typeof TextDecoder !== 'undefined') { + return new TextDecoder(encoding).decode(dataView); + } + + const a: string[] = []; + let i = 0; + + if (encoding === 'utf-8') { + /* The following algorithm is essentially a rewrite of the UTF8.decode at + http://bannister.us/weblog/2007/simple-base64-encodedecode-javascript/ + */ + + while (i < dataView.byteLength) { + let c = dataView.getUint8(i++); + + if (c < 0x80) { + // 1-byte character (7 bits) + } + else if (c < 0xe0) { + // 2-byte character (11 bits) + c = (c & 0x1f) << 6; + c |= (dataView.getUint8(i++) & 0x3f); + } + else if (c < 0xf0) { + // 3-byte character (16 bits) + c = (c & 0xf) << 12; + c |= (dataView.getUint8(i++) & 0x3f) << 6; + c |= (dataView.getUint8(i++) & 0x3f); + } + else { + // 4-byte character (21 bits) + c = (c & 0x7) << 18; + c |= (dataView.getUint8(i++) & 0x3f) << 12; + c |= (dataView.getUint8(i++) & 0x3f) << 6; + c |= (dataView.getUint8(i++) & 0x3f); + } + + a.push(String.fromCharCode(c)); + } + } + else { // Just map byte-by-byte (probably wrong) + while (i < dataView.byteLength) { + a.push(String.fromCharCode(dataView.getUint8(i++))); + } + } + + return a.join(''); +}; diff --git a/lib/src/iso/bmff/readers/readData.ts b/lib/src/iso/bmff/readers/readData.ts new file mode 100644 index 00000000..2183e174 --- /dev/null +++ b/lib/src/iso/bmff/readers/readData.ts @@ -0,0 +1,4 @@ +export function readData(dataView: DataView, offset: number, size: number): Uint8Array { + const length = (size > 0) ? size : (dataView.byteLength - (offset - dataView.byteOffset)); + return new Uint8Array(dataView.buffer, offset, Math.max(length, 0)); +}; diff --git a/lib/src/iso/bmff/readers/readInt.ts b/lib/src/iso/bmff/readers/readInt.ts new file mode 100644 index 00000000..84695081 --- /dev/null +++ b/lib/src/iso/bmff/readers/readInt.ts @@ -0,0 +1,28 @@ +export function readInt(dataView: DataView, offset: number, size: number): number { + let result = NaN; + const cursor = offset - dataView.byteOffset; + + switch (size) { + case 1: + result = dataView.getInt8(cursor); + break; + + case 2: + result = dataView.getInt16(cursor); + break; + + case 4: + result = dataView.getInt32(cursor); + break; + + case 8: + // Warning: JavaScript cannot handle 64-bit integers natively. + // This will give unexpected results for integers >= 2^53 + const s1 = dataView.getInt32(cursor); + const s2 = dataView.getInt32(cursor + 4); + result = (s1 * Math.pow(2, 32)) + s2; + break; + } + + return result; +}; diff --git a/lib/src/iso/bmff/readers/readString.ts b/lib/src/iso/bmff/readers/readString.ts new file mode 100644 index 00000000..73df2693 --- /dev/null +++ b/lib/src/iso/bmff/readers/readString.ts @@ -0,0 +1,13 @@ +import { readUint } from './readUint.js'; + +export function readString(dataView: DataView, offset: number, length: number): string { + let str = ''; + + for (let c = 0; c < length; c++) { + const cursor = offset + c; + const char = readUint(dataView, cursor, 1); + str += String.fromCharCode(char); + } + + return str; +} diff --git a/lib/src/iso/bmff/readers/readTemplate.ts b/lib/src/iso/bmff/readers/readTemplate.ts new file mode 100644 index 00000000..805dce73 --- /dev/null +++ b/lib/src/iso/bmff/readers/readTemplate.ts @@ -0,0 +1,8 @@ +import { readUint } from './readUint.js'; + +export function readTemplate(dataView: DataView, offset: number, size: number): number { + const half = size / 2; + const pre = readUint(dataView, offset, half); + const post = readUint(dataView, offset + half, half); + return pre + (post / Math.pow(2, half)); +}; diff --git a/lib/src/iso/bmff/readers/readTerminatedString.ts b/lib/src/iso/bmff/readers/readTerminatedString.ts new file mode 100644 index 00000000..b4deb05b --- /dev/null +++ b/lib/src/iso/bmff/readers/readTerminatedString.ts @@ -0,0 +1,18 @@ +import { readUint } from './readUint.js'; + +export function readTerminatedString(dataView: DataView, offset: number): string { + let str = ''; + let cursor = offset; + + while (cursor - dataView.byteOffset < dataView.byteLength) { + const char = readUint(dataView, cursor, 1); + if (char === 0) { + break; + } + + str += String.fromCharCode(char); + cursor++; + } + + return str; +}; diff --git a/lib/src/iso/bmff/readers/readUTF8String.ts b/lib/src/iso/bmff/readers/readUTF8String.ts new file mode 100644 index 00000000..4e50e904 --- /dev/null +++ b/lib/src/iso/bmff/readers/readUTF8String.ts @@ -0,0 +1,6 @@ +import { dataViewToString } from './dataViewToString.js'; + +export function readUTF8String(dataView: DataView, offset: number): string { + const length = dataView.byteLength - (offset - dataView.byteOffset); + return (length > 0) ? dataViewToString(new DataView(dataView.buffer, offset, length)) : ''; +}; diff --git a/lib/src/iso/bmff/readers/readUTF8TerminatedString.ts b/lib/src/iso/bmff/readers/readUTF8TerminatedString.ts new file mode 100644 index 00000000..e821b4a1 --- /dev/null +++ b/lib/src/iso/bmff/readers/readUTF8TerminatedString.ts @@ -0,0 +1,24 @@ +import { dataViewToString } from './dataViewToString.js'; + +export function readUTF8TerminatedString(dataView: DataView, offset: number): string { + const length = dataView.byteLength - (offset - dataView.byteOffset); + + let data = ''; + + if (length > 0) { + const view = new DataView(dataView.buffer, offset, length); + + let l = 0; + + for (; l < length; l++) { + if (view.getUint8(l) === 0) { + break; + } + } + + // remap the Dataview with the actual length + data = dataViewToString(new DataView(dataView.buffer, offset, l)); + } + + return data; +}; diff --git a/lib/src/iso/bmff/readers/readUint.ts b/lib/src/iso/bmff/readers/readUint.ts new file mode 100644 index 00000000..9bb4c4b6 --- /dev/null +++ b/lib/src/iso/bmff/readers/readUint.ts @@ -0,0 +1,37 @@ +export function readUint(dataView: DataView, offset: number, size: number): number { + const cursor = offset - dataView.byteOffset; + + let value: number = NaN; + let s1: number; + let s2: number; + + switch (size) { + case 1: + value = dataView.getUint8(cursor); + break; + + case 2: + value = dataView.getUint16(cursor); + break; + + case 3: + s1 = dataView.getUint16(cursor); + s2 = dataView.getUint8(cursor + 2); + value = (s1 << 8) + s2; + break; + + case 4: + value = dataView.getUint32(cursor); + break; + + case 8: + // Warning: JavaScript cannot handle 64-bit integers natively. + // This will give unexpected results for integers >= 2^53 + s1 = dataView.getUint32(cursor); + s2 = dataView.getUint32(cursor + 4); + value = (s1 * Math.pow(2, 32)) + s2; + break; + } + + return value; +}; diff --git a/lib/src/isobmff.ts b/lib/src/isobmff.ts new file mode 100644 index 00000000..d4167d33 --- /dev/null +++ b/lib/src/isobmff.ts @@ -0,0 +1,23 @@ +export * from './iso/bmff/Box.js'; +export * from './iso/bmff/BoxFilter.js'; +export * from './iso/bmff/BoxParser.js'; +export * from './iso/bmff/BoxParserMap.js'; +export * from './iso/bmff/createIsoView.js'; +export * from './iso/bmff/fields/DATA.js'; +export * from './iso/bmff/fields/INT.js'; +export * from './iso/bmff/fields/STRING.js'; +export * from './iso/bmff/fields/TEMPLATE.js'; +export * from './iso/bmff/fields/UINT.js'; +export * from './iso/bmff/fields/UTF8.js'; +export * from './iso/bmff/filterBoxes.js'; +export * from './iso/bmff/filterBoxesByType.js'; +export * from './iso/bmff/findBox.js'; +export * from './iso/bmff/findBoxByType.js'; +export * from './iso/bmff/FullBox.js'; +export * from './iso/bmff/IsoData.js'; +export * from './iso/bmff/IsoView.js'; +export * from './iso/bmff/IsoViewConfig.js'; +export * from './iso/bmff/parseBoxes.js'; +export * from './iso/bmff/parsers.js'; +export * from './iso/bmff/readers/ISOFieldTypeMap.js'; +export * from './iso/bmff/TypeBox.js'; diff --git a/lib/test/id3/data/PTS.ts b/lib/test/id3/data/PTS.ts index 4d70483b..bb9e5b24 100644 --- a/lib/test/id3/data/PTS.ts +++ b/lib/test/id3/data/PTS.ts @@ -10,4 +10,6 @@ export const PTS: Uint8Array = createId3('PRIV', new Uint8Array([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ])); +console.log('PTS', PTS); + export const PTS_FRAME: Id3Frame = getId3Frames(PTS)[0]; diff --git a/lib/test/id3/data/createId3.ts b/lib/test/id3/data/createId3.ts index 44c71e99..ebb32504 100644 --- a/lib/test/id3/data/createId3.ts +++ b/lib/test/id3/data/createId3.ts @@ -13,7 +13,7 @@ function createId3Size(size: number) { } export function createId3(type: string, data: Uint8Array): Uint8Array { - return new Uint8Array([ + const id3 = new Uint8Array([ //////////// // Header // //////////// @@ -46,4 +46,8 @@ export function createId3(type: string, data: Uint8Array): Uint8Array { // Payload ...data, ]); + + console.log('ID3_BYTES', id3); + + return id3; } diff --git a/lib/test/iso/bmff/ardi.test.ts b/lib/test/iso/bmff/ardi.test.ts new file mode 100644 index 00000000..2ab4b55b --- /dev/null +++ b/lib/test/iso/bmff/ardi.test.ts @@ -0,0 +1,9 @@ +import { ardi, assert, describe, findBox, it, meta, prsl } from './util/box'; +describe('ardi box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('SRMP_AC4.mp4', [ardi, prsl, meta]); + assert.ok(box); + assert.strictEqual(box.type, 'ardi'); + assert.strictEqual(box.audioRenderingIndication <= 4, true); + }); +}); diff --git a/lib/test/iso/bmff/avc1.test.ts b/lib/test/iso/bmff/avc1.test.ts new file mode 100644 index 00000000..51bade8a --- /dev/null +++ b/lib/test/iso/bmff/avc1.test.ts @@ -0,0 +1,30 @@ +import { assert, avc1, describe, filterBoxes, it, stsd } from './util/box'; + +describe('avc1 box', function () { + it('should correctly parse the box', function () { + const container = filterBoxes('240fps_go_pro_hero_4.mp4', [stsd, avc1]); + const box = container[0].entries[0]; + + assert.strictEqual(box.type, 'avc1'); + assert.strictEqual(box.size, 184); + + assert.deepStrictEqual(box.reserved1, [0, 0, 0, 0, 0, 0]); + assert.strictEqual(box.dataReferenceIndex, 1); + assert.strictEqual(box.preDefined1, 0); + assert.strictEqual(box.reserved2, 0); + assert.deepStrictEqual(box.preDefined2, [0, 0, 0]); + assert.strictEqual(box.width, 1280); + assert.strictEqual(box.height, 720); + assert.strictEqual(box.horizresolution, 72); + assert.strictEqual(box.vertresolution, 72); + assert.strictEqual(box.reserved3, 0); + assert.strictEqual(box.frameCount, 1); + assert.deepStrictEqual(box.compressorName, [0x11, 0x47, 0x6F, 0x50, 0x72, 0x6F, 0x20, 0x41, + 0x56, 0x43, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, + 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // length + 'GoPro AVC encoder' + assert.strictEqual(box.depth, 24); + assert.strictEqual(box.preDefined3, -1); + assert.strictEqual(box.config.byteLength, 98); + }); +}); diff --git a/lib/test/iso/bmff/ctts.test.ts b/lib/test/iso/bmff/ctts.test.ts new file mode 100644 index 00000000..b06a1ad6 --- /dev/null +++ b/lib/test/iso/bmff/ctts.test.ts @@ -0,0 +1,27 @@ +import { assert, ctts, describe, filterBoxes, it } from './util/box'; + +describe('ctts box', function () { + it('should correctly parse the box', function () { + const boxes = filterBoxes('time_to_sample.mp4', ctts); + const box = boxes[0]; + + assert.strictEqual(boxes.length, 1); + + assert.strictEqual(box.type, 'ctts'); + + assert.strictEqual(box.entryCount, 5); + + const { entries } = box; + assert.strictEqual(entries.length, 5); + assert.strictEqual(entries[0].sampleCount, 1); + assert.strictEqual(entries[0].sampleOffset, 1024); + assert.strictEqual(entries[1].sampleCount, 1); + assert.strictEqual(entries[1].sampleOffset, 2560); + assert.strictEqual(entries[2].sampleCount, 1); + assert.strictEqual(entries[2].sampleOffset, 1024); + assert.strictEqual(entries[3].sampleCount, 1); + assert.strictEqual(entries[3].sampleOffset, 0); + assert.strictEqual(entries[4].sampleCount, 1); + assert.strictEqual(entries[4].sampleOffset, 512); + }); +}); diff --git a/lib/test/iso/bmff/dref.test.ts b/lib/test/iso/bmff/dref.test.ts new file mode 100644 index 00000000..b0ba3e46 --- /dev/null +++ b/lib/test/iso/bmff/dref.test.ts @@ -0,0 +1,18 @@ +import { assert, describe, dref, filterBoxes, it, url } from './util/box'; + +describe('dref box', function () { + it('should correctly parse the box from sample data', function () { + const boxes = filterBoxes('captions.mp4', [dref, url]); + assert.strictEqual(boxes.length, 1); + + const box = boxes[0]; + assert.strictEqual(box.type, 'dref'); + assert.strictEqual(box.entries.length, 1); + + const entry = box.entries[0]; + assert.strictEqual(entry.type, 'url '); + assert.strictEqual(entry.version, 0); + assert.strictEqual(entry.flags, 1); + assert.strictEqual(entry.location, ''); + }); +}); diff --git a/lib/test/iso/bmff/elng.test.ts b/lib/test/iso/bmff/elng.test.ts new file mode 100644 index 00000000..c0227405 --- /dev/null +++ b/lib/test/iso/bmff/elng.test.ts @@ -0,0 +1,10 @@ +import { assert, describe, elng, findBox, it, meta, prsl } from './util/box'; + +describe('elng box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('SRMP_AC4.mp4', [elng, meta, prsl]); + + assert.strictEqual(box.type, 'elng'); + assert.strictEqual(box.extendedLanguage.localeCompare('en'), 0); + }); +}); diff --git a/lib/test/iso/bmff/elst.test.ts b/lib/test/iso/bmff/elst.test.ts new file mode 100644 index 00000000..c9428234 --- /dev/null +++ b/lib/test/iso/bmff/elst.test.ts @@ -0,0 +1,14 @@ +import { assert, describe, elst, findBox, it } from './util/box'; + +describe('elst box', function () { + it('should correctly parse the box', function () { + const box = findBox('editlist.mp4', elst); + + assert.strictEqual(box.size, 28); + assert.strictEqual(box.entryCount, 1); + assert.strictEqual(box.entries[0].segmentDuration, 1000); + assert.strictEqual(box.entries[0].mediaRateInteger, 1); + assert.strictEqual(box.entries[0].mediaRateFraction, 0); + assert.strictEqual(box.entries[0].mediaTime, 1024); + }); +}); diff --git a/lib/test/iso/bmff/emsg.test.ts b/lib/test/iso/bmff/emsg.test.ts new file mode 100644 index 00000000..ab25f491 --- /dev/null +++ b/lib/test/iso/bmff/emsg.test.ts @@ -0,0 +1,18 @@ +import { assert, describe, emsg, findBox, it, type EventMessageBox } from './util/box'; + +describe('emsg box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('emsg.m4s', [emsg]); + + assert.strictEqual(box.type, 'emsg'); + assert.strictEqual(box.size, 74); + assert.strictEqual(box.version, 0); + assert.strictEqual(box.schemeIdUri, 'urn:mpeg:dash:event:2012'); + assert.strictEqual(box.value, '1'); + assert.strictEqual(box.eventDuration, 65535); + assert.strictEqual(box.id, 1); + assert.strictEqual(box.timescale, 1); + assert.strictEqual(box.presentationTimeDelta, 1); + assert.deepEqual(box.messageData, new Uint8Array([50, 48, 49, 52, 45, 48, 54, 45, 49, 49, 84, 49, 51, 58, 48, 54, 58, 50, 52])); + }); +}); diff --git a/lib/test/iso/bmff/filterBoxesByType.test.ts b/lib/test/iso/bmff/filterBoxesByType.test.ts new file mode 100644 index 00000000..f11f825c --- /dev/null +++ b/lib/test/iso/bmff/filterBoxesByType.test.ts @@ -0,0 +1,12 @@ +import { assert, describe, filterBoxesByType, it } from './util/box'; +import { load } from './util/load'; + +describe('filter boxes by type', function () { + it('filter boxes by type', function () { + const buffer = load('./captions_fragmented.mp4'); + const boxes = filterBoxesByType('mdat', buffer); + + assert.strictEqual(boxes.length, 158); + assert.strictEqual(boxes.every(box => box.type === 'mdat'), true); + }); +}); diff --git a/lib/test/iso/bmff/findBoxByType.test.ts b/lib/test/iso/bmff/findBoxByType.test.ts new file mode 100644 index 00000000..3e676cdf --- /dev/null +++ b/lib/test/iso/bmff/findBoxByType.test.ts @@ -0,0 +1,13 @@ +import { assert, describe, findBoxByType, it } from './util/box'; +import { load } from './util/load'; + +describe('find a box by type', function () { + it('find a box by type', function () { + // Sample 'ftyp' box (20 bytes) + const buffer = load('./captions.mp4'); + const box = findBoxByType('mdat', buffer); + + assert.strictEqual(box.type, 'mdat'); + assert.strictEqual(box.size, 21530); + }); +}); diff --git a/lib/test/iso/bmff/fixtures/240fps_go_pro_hero_4.mp4 b/lib/test/iso/bmff/fixtures/240fps_go_pro_hero_4.mp4 new file mode 100644 index 00000000..1d7d148b Binary files /dev/null and b/lib/test/iso/bmff/fixtures/240fps_go_pro_hero_4.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/SRMP_AC4.mp4 b/lib/test/iso/bmff/fixtures/SRMP_AC4.mp4 new file mode 100644 index 00000000..aaa35766 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/SRMP_AC4.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/captions.mp4 b/lib/test/iso/bmff/fixtures/captions.mp4 new file mode 100644 index 00000000..9ed445bf Binary files /dev/null and b/lib/test/iso/bmff/fixtures/captions.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/captions_fragmented.mp4 b/lib/test/iso/bmff/fixtures/captions_fragmented.mp4 new file mode 100644 index 00000000..0425def0 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/captions_fragmented.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/dash-chunks-prft.m4s b/lib/test/iso/bmff/fixtures/dash-chunks-prft.m4s new file mode 100644 index 00000000..563438b6 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/dash-chunks-prft.m4s differ diff --git a/lib/test/iso/bmff/fixtures/editlist.mp4 b/lib/test/iso/bmff/fixtures/editlist.mp4 new file mode 100644 index 00000000..50e445a1 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/editlist.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/emsg.m4s b/lib/test/iso/bmff/fixtures/emsg.m4s new file mode 100644 index 00000000..ca4b5025 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/emsg.m4s differ diff --git a/lib/test/iso/bmff/fixtures/hvc1_init.mp4 b/lib/test/iso/bmff/fixtures/hvc1_init.mp4 new file mode 100644 index 00000000..026693a2 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/hvc1_init.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/mss_moof_tfdt.mp4 b/lib/test/iso/bmff/fixtures/mss_moof_tfdt.mp4 new file mode 100644 index 00000000..91f3a4cf Binary files /dev/null and b/lib/test/iso/bmff/fixtures/mss_moof_tfdt.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/spliced_10000.m4v b/lib/test/iso/bmff/fixtures/spliced_10000.m4v new file mode 100644 index 00000000..0d5dbb3e Binary files /dev/null and b/lib/test/iso/bmff/fixtures/spliced_10000.m4v differ diff --git a/lib/test/iso/bmff/fixtures/subsample.m4s b/lib/test/iso/bmff/fixtures/subsample.m4s new file mode 100644 index 00000000..2fdb3756 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/subsample.m4s differ diff --git a/lib/test/iso/bmff/fixtures/test_frag.mp4 b/lib/test/iso/bmff/fixtures/test_frag.mp4 new file mode 100644 index 00000000..4d8b69ba Binary files /dev/null and b/lib/test/iso/bmff/fixtures/test_frag.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/time_to_sample.mp4 b/lib/test/iso/bmff/fixtures/time_to_sample.mp4 new file mode 100644 index 00000000..626907ab Binary files /dev/null and b/lib/test/iso/bmff/fixtures/time_to_sample.mp4 differ diff --git a/lib/test/iso/bmff/fixtures/webvtt.m4s b/lib/test/iso/bmff/fixtures/webvtt.m4s new file mode 100644 index 00000000..a48ae159 Binary files /dev/null and b/lib/test/iso/bmff/fixtures/webvtt.m4s differ diff --git a/lib/test/iso/bmff/free.test.ts b/lib/test/iso/bmff/free.test.ts new file mode 100644 index 00000000..30e2af4f --- /dev/null +++ b/lib/test/iso/bmff/free.test.ts @@ -0,0 +1,11 @@ +import { assert, describe, free, it, parseBox } from './util/box'; + +describe('free box', () => { + it('should correctly parse the box', () => { + const box = parseBox('captions.mp4', free, 3); + + assert.strictEqual(box.type, 'free'); + assert.strictEqual(box.size, 59); + assert.strictEqual(box.data.length, 51); + }); +}); diff --git a/lib/test/iso/bmff/ftyp.test.ts b/lib/test/iso/bmff/ftyp.test.ts new file mode 100644 index 00000000..50e5353d --- /dev/null +++ b/lib/test/iso/bmff/ftyp.test.ts @@ -0,0 +1,13 @@ +import { ftyp } from '@svta/common-media-library'; +import { assert, describe, it, parseBox } from './util/box'; +describe('ftyp box', function () { + it('should correctly parse the box', function () { + const box = parseBox('captions.mp4', ftyp, 0); + + assert.strictEqual(box.type, 'ftyp'); + assert.strictEqual(box.size, 20); + assert.strictEqual(box.majorBrand, 'isom'); + assert.strictEqual(box.minorVersion, 1); + assert.deepStrictEqual(box.compatibleBrands, ['isom']); + }); +}); diff --git a/lib/test/iso/bmff/hdlr.test.ts b/lib/test/iso/bmff/hdlr.test.ts new file mode 100644 index 00000000..0c8995ad --- /dev/null +++ b/lib/test/iso/bmff/hdlr.test.ts @@ -0,0 +1,23 @@ +import { hdlr } from '@svta/common-media-library'; +import { assert, describe, findBox, it } from './util/box'; + +describe('hdlr box', function () { + it('should correctly parse the box', function () { + const box = findBox('captions.mp4', hdlr); + + assert.strictEqual(box.type, 'hdlr'); + assert.strictEqual(box.size, 68); + + assert.strictEqual(box.preDefined, 0); + assert.strictEqual(box.handlerType, 'subt'); + assert.deepStrictEqual(box.reserved, [0, 0, 0]); + assert.strictEqual(box.name, '*xml:ext=ttml@GPAC0.5.1-DEV-rev5545'); + }); + + it('should handle null-terminated strings that are not null-terminated and might exceed box boundaries', function () { + const box = findBox('240fps_go_pro_hero_4.mp4', hdlr); + + assert.ok(box); + assert.strictEqual(box.name, '\tGoPro AVC'); + }); +}); diff --git a/lib/test/iso/bmff/hvc1.test.ts b/lib/test/iso/bmff/hvc1.test.ts new file mode 100644 index 00000000..02454e98 --- /dev/null +++ b/lib/test/iso/bmff/hvc1.test.ts @@ -0,0 +1,30 @@ +import { assert, describe, filterBoxes, hvc1, it, stsd } from './util/box'; + +describe('hvc1 box', function () { + it('should correctly parse the box', function () { + const container = filterBoxes('hvc1_init.mp4', [stsd, hvc1]); + const box = container[0].entries[0]; + + assert.strictEqual(box.type, 'hvc1'); + assert.strictEqual(box.size, 137); + + assert.deepStrictEqual(box.reserved1, [0, 0, 0, 0, 0, 0]); + assert.strictEqual(box.dataReferenceIndex, 1); + assert.strictEqual(box.preDefined1, 0); + assert.strictEqual(box.reserved2, 0); + assert.deepStrictEqual(box.preDefined2, [0, 0, 0]); + assert.strictEqual(box.width, 192); + assert.strictEqual(box.height, 108); + assert.strictEqual(box.horizresolution, 72); + assert.strictEqual(box.vertresolution, 72); + assert.strictEqual(box.reserved3, 0); + assert.strictEqual(box.frameCount, 1); + assert.deepStrictEqual(box.compressorName, [0x0B, 0x48, 0x45, 0x56, 0x43, 0x20, 0x43, 0x6F, + 0x64, 0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // length + 'HEVC Coding' + assert.strictEqual(box.depth, 24); + assert.strictEqual(box.preDefined3, -1); + assert.strictEqual(box.config.byteLength, 51); + }); +}); diff --git a/lib/test/iso/bmff/kind.test.ts b/lib/test/iso/bmff/kind.test.ts new file mode 100644 index 00000000..1e4bdf60 --- /dev/null +++ b/lib/test/iso/bmff/kind.test.ts @@ -0,0 +1,11 @@ +import { assert, describe, findBox, it, kind, meta, prsl } from './util/box'; + +describe('kind box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('SRMP_AC4.mp4', [kind, meta, prsl]); + + assert.strictEqual(box.type, 'kind'); + assert.strictEqual(box.schemeUri, 'urn:mpeg:dash:role:2011'); + assert.strictEqual(box.value, 'main'); + }); +}); diff --git a/lib/test/iso/bmff/labl.test.ts b/lib/test/iso/bmff/labl.test.ts new file mode 100644 index 00000000..1414f641 --- /dev/null +++ b/lib/test/iso/bmff/labl.test.ts @@ -0,0 +1,23 @@ +import { assert, describe, findBox, it, labl, meta, prsl, type Box } from './util/box'; + +describe('labl box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('SRMP_AC4.mp4', [meta, prsl, labl]) + .boxes?.filter((box: Box) => box.type === 'grpl')[0] + .boxes?.filter((box: Box) => box.groupId === 234)[0]; + + assert.ok(box); + assert.ok(box.boxes); + + const boxes = box.boxes.filter((box: Box) => box.type === 'labl'); + + assert.strictEqual(boxes[0].type, 'labl'); + assert.strictEqual(boxes[0].isGroupLabel, false); + assert.strictEqual(boxes[0].language, 'en'); + assert.strictEqual(boxes[0].label, 'Spanish'); + assert.strictEqual(boxes[1].type, 'labl'); + assert.strictEqual(boxes[1].isGroupLabel, false); + assert.strictEqual(boxes[1].language, 'es'); + assert.strictEqual(boxes[1].label, 'EspaƱol'); + }); +}); diff --git a/lib/test/iso/bmff/mdat.test.ts b/lib/test/iso/bmff/mdat.test.ts new file mode 100644 index 00000000..d233b64b --- /dev/null +++ b/lib/test/iso/bmff/mdat.test.ts @@ -0,0 +1,12 @@ +import { mdat } from '@svta/common-media-library'; +import { assert, describe, it, parseBox } from './util/box'; + +describe('mdat box', function () { + it('should correctly parse the mdat box', function () { + const box = parseBox('captions.mp4', mdat, 2); + + assert.strictEqual(box.type, 'mdat'); + assert.strictEqual(box.size, 21530); + assert.strictEqual(box.data.byteLength, 21522); + }); +}); diff --git a/lib/test/iso/bmff/mdhd.test.ts b/lib/test/iso/bmff/mdhd.test.ts new file mode 100644 index 00000000..26ab4e3c --- /dev/null +++ b/lib/test/iso/bmff/mdhd.test.ts @@ -0,0 +1,14 @@ +import { assert, describe, findBox, it, mdhd, type MediaHeaderBox } from './util/box'; + +describe('mdhd box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('captions.mp4', [mdhd]); + + assert.strictEqual(box.creationTime, 3507186411); + assert.strictEqual(box.modificationTime, 3507186411); + assert.strictEqual(box.timescale, 1000); + assert.strictEqual(box.duration, 629800); + assert.strictEqual(box.language, 'und'); + assert.strictEqual(box.preDefined, 0); + }); +}); diff --git a/lib/test/iso/bmff/mehd.test.ts b/lib/test/iso/bmff/mehd.test.ts new file mode 100644 index 00000000..843321b0 --- /dev/null +++ b/lib/test/iso/bmff/mehd.test.ts @@ -0,0 +1,12 @@ +import { mehd } from '@svta/common-media-library'; +import { assert, describe, findBox, it } from './util/box'; + +describe('mehd box', function () { + it('should correctly parse the box', function () { + const box = findBox('test_frag.mp4', mehd); + + assert.strictEqual(box.type, 'mehd'); + assert.strictEqual(box.size, 16); + assert.strictEqual(box.fragmentDuration, 2047); + }); +}); diff --git a/lib/test/iso/bmff/mfro.test.ts b/lib/test/iso/bmff/mfro.test.ts new file mode 100644 index 00000000..fb88328b --- /dev/null +++ b/lib/test/iso/bmff/mfro.test.ts @@ -0,0 +1,12 @@ +import { mfro } from '@svta/common-media-library'; +import { assert, describe, findBox, it } from './util/box'; + +describe('mfro box', function () { + it('should correctly parse the box', function () { + const box = findBox('test_frag.mp4', mfro); + + assert.strictEqual(box.type, 'mfro'); + assert.strictEqual(box.size, 16); + assert.strictEqual(box.mfra_size, 105); + }); +}); diff --git a/lib/test/iso/bmff/moov.test.ts b/lib/test/iso/bmff/moov.test.ts new file mode 100644 index 00000000..b80b4bc9 --- /dev/null +++ b/lib/test/iso/bmff/moov.test.ts @@ -0,0 +1,12 @@ +import { assert, describe, it, parseContainer } from './util/box'; + +describe('moov box', function () { + it('should correctly parse the box', function () { + const box = parseContainer('captions.mp4', -3); + + assert.ok(box); + assert.strictEqual(box.type, 'moov'); + assert.strictEqual(box.size, 1028); + assert.strictEqual(box.boxes.length, 2); + }); +}); diff --git a/lib/test/iso/bmff/mp4a.test.ts b/lib/test/iso/bmff/mp4a.test.ts new file mode 100644 index 00000000..fcd45f98 --- /dev/null +++ b/lib/test/iso/bmff/mp4a.test.ts @@ -0,0 +1,21 @@ +import { assert, describe, filterBoxes, it, mp4a, stsd } from './util/box'; + +describe('mp4a box', function () { + it('should correctly parse the box', function () { + const container = filterBoxes('240fps_go_pro_hero_4.mp4', [stsd, mp4a]); + const box = container[1].entries[0]; + + assert.strictEqual(box.type, 'mp4a'); + assert.strictEqual(box.size, 86); + + assert.deepStrictEqual(box.reserved1, [0, 0, 0, 0, 0, 0]); + assert.strictEqual(box.dataReferenceIndex, 1); + assert.deepStrictEqual(box.reserved2, [0, 0]); + assert.strictEqual(box.channelcount, 2); + assert.strictEqual(box.samplesize, 16); + //assert.strictEqual(box.pre_defined, 0); // not conformed value in the file, not tested + assert.strictEqual(box.reserved3, 0); + assert.strictEqual(box.samplerate, 48000); + assert.strictEqual(box.esds.byteLength, 50); + }); +}); diff --git a/lib/test/iso/bmff/parseBoxes.test.ts b/lib/test/iso/bmff/parseBoxes.test.ts new file mode 100644 index 00000000..c4c1a66f --- /dev/null +++ b/lib/test/iso/bmff/parseBoxes.test.ts @@ -0,0 +1,24 @@ +import { assert, describe, ftyp, it, parseBoxes } from './util/box'; + +describe('parseBoxes', function () { + it('should parse a buffer', function () { + // Sample 'ftyp' box (20 bytes) + const arrayBuffer = new Uint8Array([0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x69, 0x73, 0x6f, 0x6d]).buffer; + const boxes = parseBoxes(arrayBuffer, { parsers: { ftyp } }); + const box = boxes[0]; + + assert.strictEqual(boxes.length, 1); + assert.strictEqual(box.type, 'ftyp'); + assert.strictEqual(box.size, 20); + assert.strictEqual(box.majorBrand, 'isom'); + assert.strictEqual(box.minorVersion, 1); + assert.deepEqual(box.compatibleBrands, ['isom']); + }); + + it('should exit the parser if garbage is zero', function () { + const zero = new Uint8Array(1500); + const boxes = parseBoxes(zero.buffer); + assert.strictEqual(boxes.length, 1); + assert.strictEqual(boxes[0].size, 0); + }); +}); diff --git a/lib/test/iso/bmff/payl.test.ts b/lib/test/iso/bmff/payl.test.ts new file mode 100644 index 00000000..38a3975d --- /dev/null +++ b/lib/test/iso/bmff/payl.test.ts @@ -0,0 +1,13 @@ +import { assert, describe, findBox, it, mdat, parseBoxes, payl } from './util/box'; + +describe('payl box', function () { + it('should correctly parse the box from sample data', function () { + const { data } = findBox('webvtt.m4s', mdat); + const boxes = parseBoxes(data, { parsers: { payl } }); + const box = boxes[0].boxes?.[0]; + + assert.ok(box); + assert.strictEqual(box.type, 'payl'); + assert.strictEqual(box.cueText, "You're a jerk, Thom.\n"); + }); +}); diff --git a/lib/test/iso/bmff/prft.test.ts b/lib/test/iso/bmff/prft.test.ts new file mode 100644 index 00000000..c72f998e --- /dev/null +++ b/lib/test/iso/bmff/prft.test.ts @@ -0,0 +1,13 @@ +import { assert, describe, filterBoxes, it, prft } from './util/box'; + +describe('prft box', function () { + it('should correctly parse the box from sample data', function () { + const boxes = filterBoxes('dash-chunks-prft.m4s', prft); + assert.strictEqual(boxes.length, 60); + assert.strictEqual(boxes[0].type, 'prft'); + assert.strictEqual(boxes[0].referenceTrackId, 1); + assert.strictEqual(boxes[0].ntpTimestampSec, 3879495203); + assert.strictEqual(boxes[0].ntpTimestampFrac, 197568495); + assert.strictEqual(boxes[0].mediaTime, 1355974620); + }); +}); diff --git a/lib/test/iso/bmff/prsl.test.ts b/lib/test/iso/bmff/prsl.test.ts new file mode 100644 index 00000000..33527196 --- /dev/null +++ b/lib/test/iso/bmff/prsl.test.ts @@ -0,0 +1,15 @@ +import { assert, describe, filterBoxes, it, meta, prsl } from './util/box'; + +describe('prsl box', function () { + it('should correctly parse the prsl box from sample data', function () { + const boxes = filterBoxes('SRMP_AC4.mp4', [prsl, meta]); + + assert.strictEqual(boxes.length, 6); + assert.strictEqual(boxes[1].type, 'prsl'); + assert.strictEqual(boxes[1].groupId, 234); + assert.strictEqual(boxes[1].numEntitiesInGroup, 1); + assert.strictEqual(boxes[1].entities[0].entityId, 1); + assert.strictEqual(boxes[1].preselectionTag, '1'); + assert.strictEqual(boxes[1].selectionPriority, 1); + }); +}); diff --git a/lib/test/iso/bmff/smhd.test.ts b/lib/test/iso/bmff/smhd.test.ts new file mode 100644 index 00000000..c42b069a --- /dev/null +++ b/lib/test/iso/bmff/smhd.test.ts @@ -0,0 +1,10 @@ +import { assert, describe, filterBoxes, it, smhd } from './util/box'; + +describe('smhd box', function () { + it('should correctly parse the box from sample data', function () { + const boxes = filterBoxes('240fps_go_pro_hero_4.mp4', smhd); + assert.strictEqual(boxes.length, 1); + assert.strictEqual(boxes[0].type, 'smhd'); + assert.strictEqual(boxes[0].balance, 0.0); + }); +}); diff --git a/lib/test/iso/bmff/ssix.test.ts b/lib/test/iso/bmff/ssix.test.ts new file mode 100644 index 00000000..400cba7c --- /dev/null +++ b/lib/test/iso/bmff/ssix.test.ts @@ -0,0 +1,17 @@ +import { assert, describe, it, parseBox, ssix } from './util/box'; + +describe('ssix box', function () { + it('should correctly parse the box', function () { + const box = parseBox('spliced_10000.m4v', ssix, 4); + + assert.strictEqual(box.type, 'ssix'); + assert.strictEqual(box.size, 8124); + assert.strictEqual(box.subsegmentCount, 25); + assert.strictEqual(box.subsegments.length, 25); + + // Test one of the subsegments + assert.strictEqual(box.subsegments[0].rangesCount, 70); + assert.strictEqual(box.subsegments[0].ranges[45].level, 2); + assert.strictEqual(box.subsegments[0].ranges[45].rangeSize, 7312); + }); +}); diff --git a/lib/test/iso/bmff/stsd.test.ts b/lib/test/iso/bmff/stsd.test.ts new file mode 100644 index 00000000..bd14e980 --- /dev/null +++ b/lib/test/iso/bmff/stsd.test.ts @@ -0,0 +1,12 @@ +import { assert, describe, filterBoxes, it, stsd } from './util/box'; + +describe('stsd box', function () { + it('should correctly parse the box', function () { + const boxes = filterBoxes('240fps_go_pro_hero_4.mp4', stsd); + + assert.strictEqual(boxes.length, 3); + assert.strictEqual(boxes[0].entries[0].type, 'avc1'); + assert.strictEqual(boxes[1].entries[0].type, 'mp4a'); + assert.strictEqual(boxes[2].entries[0].type, 'fdsc'); + }); +}); diff --git a/lib/test/iso/bmff/stts.test.ts b/lib/test/iso/bmff/stts.test.ts new file mode 100644 index 00000000..52212fbe --- /dev/null +++ b/lib/test/iso/bmff/stts.test.ts @@ -0,0 +1,18 @@ +import { assert, describe, filterBoxes, it, stts } from './util/box'; + +describe('stts box', function () { + it('should correctly parse the box', function () { + const boxes = filterBoxes('editlist.mp4', stts); + const box = boxes[0]; + + assert.strictEqual(boxes.length, 1); + + assert.strictEqual(box.type, 'stts'); + assert.strictEqual(box.entryCount, 2); + assert.strictEqual(box.entries.length, 2); + assert.strictEqual(box.entries[0].sampleCount, 47); + assert.strictEqual(box.entries[0].sampleDelta, 1024); + assert.strictEqual(box.entries[1].sampleCount, 1); + assert.strictEqual(box.entries[1].sampleDelta, 896); + }); +}); diff --git a/lib/test/iso/bmff/subs.test.ts b/lib/test/iso/bmff/subs.test.ts new file mode 100644 index 00000000..ed76e486 --- /dev/null +++ b/lib/test/iso/bmff/subs.test.ts @@ -0,0 +1,24 @@ +import { assert, describe, filterBoxes, it, subs } from './util/box'; + +describe('subs box', function () { + it('should correctly parse the box from sample data', function () { + const boxes = filterBoxes('subsample.m4s', subs); + + assert.strictEqual(boxes.length, 1); + assert.strictEqual(boxes[0].type, 'subs'); + + const { entries } = boxes[0]; + assert.strictEqual(entries.length, 1); + + const entry = entries[0]; + assert.strictEqual(entry.sampleDelta, 1); + assert.strictEqual(entry.subsampleCount, 3); + assert.strictEqual(entry.subsamples.length, 3); + + const subsample = entry.subsamples[0]; + assert.strictEqual(subsample.subsampleSize, 5); + assert.strictEqual(subsample.subsamplePriority, 0); + assert.strictEqual(subsample.discardable, 0); + assert.strictEqual(subsample.codecSpecificParameters, 0); + }); +}); diff --git a/lib/test/iso/bmff/trex.test.ts b/lib/test/iso/bmff/trex.test.ts new file mode 100644 index 00000000..6f960de1 --- /dev/null +++ b/lib/test/iso/bmff/trex.test.ts @@ -0,0 +1,25 @@ +import { assert, describe, filterBoxes, it, trex } from './util/box'; + +describe('trex box', function () { + it('should correctly parse the box', function () { + const boxes = filterBoxes('test_frag.mp4', trex); + + assert.strictEqual(boxes.length, 2); + + assert.strictEqual(boxes[0].type, 'trex'); + assert.strictEqual(boxes[0].size, 32); + assert.strictEqual(boxes[0].trackId, 1); + assert.strictEqual(boxes[0].defaultSampleDescriptionIndex, 1); + assert.strictEqual(boxes[0].defaultSampleDuration, 0); + assert.strictEqual(boxes[0].defaultSampleSize, 0); + assert.strictEqual(boxes[0].defaultSampleFlags, 0); + + assert.strictEqual(boxes[1].type, 'trex'); + assert.strictEqual(boxes[1].size, 32); + assert.strictEqual(boxes[1].trackId, 2); + assert.strictEqual(boxes[1].defaultSampleDescriptionIndex, 1); + assert.strictEqual(boxes[1].defaultSampleDuration, 0); + assert.strictEqual(boxes[1].defaultSampleSize, 0); + assert.strictEqual(boxes[1].defaultSampleFlags, 0); + }); +}); diff --git a/lib/test/iso/bmff/trun.test.ts b/lib/test/iso/bmff/trun.test.ts new file mode 100644 index 00000000..a9180347 --- /dev/null +++ b/lib/test/iso/bmff/trun.test.ts @@ -0,0 +1,17 @@ +import { assert, describe, findBox, it, trun, type TrackRunBox } from './util/box'; + +describe('trun box', function () { + it('should correctly parse the box from sample data', function () { + const box = findBox('mss_moof_tfdt.mp4', [trun]); + + assert.strictEqual(box.sampleCount, 93); + assert.strictEqual(box.dataOffset, 856); + assert.strictEqual(box.firstSampleFlags, undefined); + + const sample = box.samples[0]; + assert.strictEqual(sample.sampleDuration, 213334); + assert.strictEqual(sample.sampleSize, 247); + assert.strictEqual(sample.sampleFlags, undefined); + assert.strictEqual(sample.sampleCompositionTimeOffset, undefined); + }); +}); diff --git a/lib/test/iso/bmff/util/box.ts b/lib/test/iso/bmff/util/box.ts new file mode 100644 index 00000000..0a8c4071 --- /dev/null +++ b/lib/test/iso/bmff/util/box.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { filterBoxes } from './filterBoxes'; +import { findBox } from './findBox'; +import { parseBox } from './parseBox'; +import { parseContainer } from './parseContainer'; +import { parseFile } from './parseFile'; + +export * from '@svta/common-media-library/isobmff'; +export { assert, describe, filterBoxes, findBox, it, parseBox, parseContainer, parseFile }; + diff --git a/lib/test/iso/bmff/util/createParsers.ts b/lib/test/iso/bmff/util/createParsers.ts new file mode 100644 index 00000000..50cb40d1 --- /dev/null +++ b/lib/test/iso/bmff/util/createParsers.ts @@ -0,0 +1,15 @@ +import type { BoxParser, BoxParserMap } from './box'; + +export function createParsers(parsers: BoxParser | BoxParser[]): { name: string, parsers: BoxParserMap } { + if (!Array.isArray(parsers)) { + parsers = [parsers]; + } + + return { + name: parsers[0].name, + parsers: parsers.reduce((acc, parser) => { + acc[parser.name] = parser; + return acc; + }, {} as BoxParserMap), + }; +} diff --git a/lib/test/iso/bmff/util/filterBoxes.ts b/lib/test/iso/bmff/util/filterBoxes.ts new file mode 100644 index 00000000..5c0417b1 --- /dev/null +++ b/lib/test/iso/bmff/util/filterBoxes.ts @@ -0,0 +1,8 @@ +import { filterBoxesByType, type Box, type BoxParser } from '@svta/common-media-library'; +import { createParsers } from './createParsers'; +import { load } from './load'; + +export function filterBoxes(file: string, boxParsers: BoxParser | BoxParser[]): Box[] { + const { name, parsers } = createParsers(boxParsers); + return filterBoxesByType(name, load(file), { parsers, recursive: true }); +} diff --git a/lib/test/iso/bmff/util/findBox.ts b/lib/test/iso/bmff/util/findBox.ts new file mode 100644 index 00000000..24ac9e04 --- /dev/null +++ b/lib/test/iso/bmff/util/findBox.ts @@ -0,0 +1,13 @@ +import { findBoxByType, type Box, type BoxParser } from '@svta/common-media-library'; +import assert from 'node:assert'; +import { createParsers } from './createParsers'; +import { load } from './load'; + +export function findBox(file: string, boxParsers: BoxParser | BoxParser[]): Box { + const { name, parsers } = createParsers(boxParsers); + const box = findBoxByType(name, load(file), { parsers, recursive: true }); + + assert.ok(box); + + return box; +} diff --git a/lib/test/iso/bmff/util/load.ts b/lib/test/iso/bmff/util/load.ts new file mode 100644 index 00000000..2460375a --- /dev/null +++ b/lib/test/iso/bmff/util/load.ts @@ -0,0 +1,5 @@ +import fs from 'node:fs'; + +export function load(file: string): ArrayBuffer { + return new Uint8Array(fs.readFileSync(`./test/isobmff/fixtures/${file}`)).buffer; +} diff --git a/lib/test/iso/bmff/util/parseBox.ts b/lib/test/iso/bmff/util/parseBox.ts new file mode 100644 index 00000000..f6d1dc04 --- /dev/null +++ b/lib/test/iso/bmff/util/parseBox.ts @@ -0,0 +1,6 @@ +import type { Box, BoxParser } from '@svta/common-media-library'; +import { parseFile } from './parseFile'; + +export function parseBox(file: string, parser: BoxParser, index: number): Box { + return parseFile(file, { parsers: { [parser.name]: parser } })[index]; +} diff --git a/lib/test/iso/bmff/util/parseContainer.ts b/lib/test/iso/bmff/util/parseContainer.ts new file mode 100644 index 00000000..f9295881 --- /dev/null +++ b/lib/test/iso/bmff/util/parseContainer.ts @@ -0,0 +1,6 @@ +import type { Box } from '@svta/common-media-library'; +import { parseFile } from './parseFile'; + +export function parseContainer(file: string, index: number): Box | null { + return parseFile(file, { recursive: false }).at(index) || null; +} diff --git a/lib/test/iso/bmff/util/parseFile.ts b/lib/test/iso/bmff/util/parseFile.ts new file mode 100644 index 00000000..fe58a48c --- /dev/null +++ b/lib/test/iso/bmff/util/parseFile.ts @@ -0,0 +1,6 @@ +import { parseBoxes, type Box, type IsoViewConfig } from '@svta/common-media-library'; +import { load } from './load'; + +export function parseFile(file: string, config: IsoViewConfig): Box[] { + return parseBoxes(load(file), config); +} diff --git a/lib/test/iso/bmff/vmhd.test.ts b/lib/test/iso/bmff/vmhd.test.ts new file mode 100644 index 00000000..a81ba559 --- /dev/null +++ b/lib/test/iso/bmff/vmhd.test.ts @@ -0,0 +1,12 @@ +import { assert, describe, filterBoxes, it, vmhd } from './util/box'; + +describe('vmhd box', function () { + it('should correctly parse the box from sample data', function () { + const boxes = filterBoxes('240fps_go_pro_hero_4.mp4', vmhd); + + assert.strictEqual(boxes.length, 1); + assert.strictEqual(boxes[0].type, 'vmhd'); + assert.strictEqual(boxes[0].graphicsmode, 0); + assert.deepStrictEqual(boxes[0].opcolor, [0, 0, 0]); + }); +}); diff --git a/lib/test/iso/bmff/vttc.test.ts b/lib/test/iso/bmff/vttc.test.ts new file mode 100644 index 00000000..9e9e98b9 --- /dev/null +++ b/lib/test/iso/bmff/vttc.test.ts @@ -0,0 +1,9 @@ +import { assert, describe, findBox, it, mdat, parseBoxes } from './util/box'; + +describe('vttc box', function () { + it('should correctly parse the box from sample data', function () { + const { data } = findBox('webvtt.m4s', mdat); + const boxes = parseBoxes(data); + assert.strictEqual(boxes[0].type, 'vttc'); + }); +}); diff --git a/lib/test/iso/bmff/vtte.test.ts b/lib/test/iso/bmff/vtte.test.ts new file mode 100644 index 00000000..39c234c3 --- /dev/null +++ b/lib/test/iso/bmff/vtte.test.ts @@ -0,0 +1,10 @@ +import { assert, describe, findBox, it, mdat, parseBoxes, vtte } from './util/box'; + +describe('vtte box', function () { + it('should correctly parse the box from sample data', function () { + const { data } = findBox('webvtt.m4s', mdat); + const boxes = parseBoxes(data, { parsers: { vtte } }); + assert.strictEqual(boxes[1].type, 'vtte'); + }); +}); + diff --git a/package-lock.json b/package-lock.json index 1b6f8e20..9c7d2617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@svta/common-media-library-workspace", - "version": "0.7.4", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@svta/common-media-library-workspace", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "workspaces": [ "lib", @@ -35,7 +35,7 @@ }, "dev": { "name": "@svta/common-media-library-dev", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "devDependencies": { "@web/dev-server": "0.4.6" @@ -43,12 +43,12 @@ }, "docs": { "name": "@svta/common-media-library-docs", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0" }, "lib": { "name": "@svta/common-media-library", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0" }, "node_modules/@babel/code-frame": { @@ -6459,7 +6459,7 @@ } }, "samples/cmaf-ham-conversion": { - "version": "0.7.4", + "version": "0.9.0", "license": "ISC", "dependencies": { "@svta/common-media-library": "*" diff --git a/package.json b/package.json index c7ff05e8..09875942 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@svta/common-media-library-workspace", - "version": "0.7.4", + "version": "0.9.0", "license": "Apache-2.0", "homepage": "https://github.com/streaming-video-technology-alliance/common-media-library", "authors": "Casey Occhialini <1508707+littlespex@users.noreply.github.com>", diff --git a/samples/cmaf-ham-conversion/package.json b/samples/cmaf-ham-conversion/package.json index 7bf1df76..5de7b2eb 100644 --- a/samples/cmaf-ham-conversion/package.json +++ b/samples/cmaf-ham-conversion/package.json @@ -1,6 +1,6 @@ { "name": "cmaf-ham-conversion", - "version": "0.7.4", + "version": "0.9.0", "description": "", "type": "module", "scripts": { diff --git a/scripts/build.mts b/scripts/build.mts index e8bc31e4..2a7895fb 100644 --- a/scripts/build.mts +++ b/scripts/build.mts @@ -1,7 +1,5 @@ import { rm } from 'node:fs/promises'; import { cmd } from './cmd.mjs'; -import { removeBlankFiles } from './removeBlankFiles.mjs'; await rm('dist', { recursive: true, force: true }); await cmd('tsc'); -await removeBlankFiles();