diff --git a/entries/all-boxes.ts b/entries/all-boxes.ts index 87749bf0..a6d9aec8 100644 --- a/entries/all-boxes.ts +++ b/entries/all-boxes.ts @@ -73,7 +73,10 @@ export * from '#/boxes/prdi'; export * from '#/boxes/prft'; export * from '#/boxes/pssh'; export * from '#/boxes/qt/clef'; +export * from '#/boxes/qt/data'; export * from '#/boxes/qt/enof'; +export * from '#/boxes/qt/ilst'; +export * from '#/boxes/qt/keys'; export * from '#/boxes/qt/prof'; export * from '#/boxes/qt/tapt'; export * from '#/boxes/rtp'; diff --git a/src/DataStream.ts b/src/DataStream.ts index fa9f6e30..8f84ca3f 100644 --- a/src/DataStream.ts +++ b/src/DataStream.ts @@ -48,6 +48,9 @@ export class DataStream { endianness: Endianness; protected position: number; + behavior: number; + + static BEHAVIOR_QTFF = 0x01; // QuickTime File Format behavior /** * DataStream reads scalars, arrays and structs of data from an ArrayBuffer. diff --git a/src/boxes/ftyp.ts b/src/boxes/ftyp.ts index f231b4fb..a3684be4 100644 --- a/src/boxes/ftyp.ts +++ b/src/boxes/ftyp.ts @@ -1,4 +1,5 @@ import { Box } from '#/box'; +import { DataStream } from '#/DataStream'; import type { MultiBufferStream } from '#/buffer'; export class ftypBox extends Box { @@ -21,6 +22,9 @@ export class ftypBox extends Box { toparse -= 4; i++; } + + // Certain Boxes/Atoms have different behavior when parsing QTFF files + if (this.major_brand.indexOf('qt') === 0) stream.behavior |= DataStream.BEHAVIOR_QTFF; } /** @bundle writing/ftyp.js */ diff --git a/src/boxes/meta.ts b/src/boxes/meta.ts index 322597fc..e9361ab4 100644 --- a/src/boxes/meta.ts +++ b/src/boxes/meta.ts @@ -5,6 +5,7 @@ import { iinfBox } from '#/boxes/iinf'; import { ilocBox } from '#/boxes/iloc'; import { irefBox } from '#/boxes/iref'; import { pitmBox } from '#/boxes/pitm'; +import { DataStream } from '#/DataStream'; import type { MultiBufferStream } from '#/buffer'; import { ContainerBox } from '#/containerBox'; @@ -34,7 +35,8 @@ export class metaBox extends FullBox { dinfs: Array; parse(stream: MultiBufferStream) { - this.parseFullHeader(stream); + // meta is a FullBox in MPEG-4 and a ContainerBox in QTFF + if (!(stream.behavior & DataStream.BEHAVIOR_QTFF)) this.parseFullHeader(stream); this.boxes = []; ContainerBox.prototype.parse.call(this, stream); } diff --git a/src/boxes/qt/data.ts b/src/boxes/qt/data.ts new file mode 100644 index 00000000..217e4cd9 --- /dev/null +++ b/src/boxes/qt/data.ts @@ -0,0 +1,112 @@ +import { Box } from '#/box'; +import { Log } from '#/log'; +import type { MultiBufferStream } from '#/buffer'; + +/* + * Parses the types above. Only implement the ones we actually + * have real world test data for. Add a test case, as you implement them. + */ +function parseItifData(type, data) { + if (type === dataBox.Types.UTF8) { + return new TextDecoder('utf-8').decode(data); + } + + const view = new DataView(data.buffer); + if (type === dataBox.Types.BE_UNSIGNED_INT) { + if (data.length === 1) { + return view.getUint8(0); + } else if (data.length === 2) { + return view.getUint16(0, false); + } else if (data.length === 4) { + return view.getUint32(0, false); + } else if (data.length === 8) { + return view.getBigUint64(0, false); + } else { + throw new Error('Unsupported ITIF_TYPE_BE_UNSIGNED_INT length ' + data.length); + } + } else if (type === dataBox.Types.BE_SIGNED_INT) { + if (data.length === 1) { + return view.getInt8(0); + } else if (data.length === 2) { + return view.getInt16(0, false); + } else if (data.length === 4) { + return view.getInt32(0, false); + } else if (data.length === 8) { + return view.getBigInt64(0, false); + } else { + throw new Error('Unsupported ITIF_TYPE_BE_SIGNED_INT length ' + data.length); + } + } else if (type === dataBox.Types.BE_FLOAT32) { + return view.getFloat32(0, false); + } + + Log.warn('DataBox', 'Unsupported or unimplemented itif data type: ' + type); + return undefined; +} + +/* + * The QTFF data Atom is typically in an ilst Box. + * https://developer.apple.com/documentation/quicktime-file-format/data_atom + */ + +export class dataBox extends Box { + static override readonly fourcc = 'data' as const; + box_name = 'DataBox' as const; + + country: number; + countryString: string | null; + language: number; + languageString: string | null; + raw: Uint8Array; + value: string | number | bigint | boolean | object | null; + valueType: number; + + /* + * itif data types + * https://developer.apple.com/documentation/quicktime-file-format/well-known_types + */ + static Types = { + RESERVED: 0, + UTF8: 1, + UTF16: 2, + SJIS: 3, + UTF8_SORT: 4, + UTF16_SORT: 5, + JPEG: 13, + PNG: 14, + BE_SIGNED_INT: 21, + BE_UNSIGNED_INT: 22, + BE_FLOAT32: 23, + BE_FLOAT64: 24, + BMP: 27, + QT_ATOM: 28, + BE_SIGNED_INT8: 65, + BE_SIGNED_INT16: 66, + BE_SIGNED_INT32: 67, + BE_FLOAT32_POINT: 70, + BE_FLOAT32_DIMENSIONS: 71, + BE_FLOAT32_RECT: 72, + BE_SIGNED_INT64: 74, + BE_UNSIGNED_INT8: 75, + BE_UNSIGNED_INT16: 76, + BE_UNSIGNED_INT32: 77, + BE_UNSIGNED_INT64: 78, + BE_FLOAT64_AFFINE_TRANSFORM: 79, + } as const; + + parse(stream: MultiBufferStream) { + this.valueType = stream.readUint32(); + this.country = stream.readUint16(); + if (this.country > 255) { + stream.seek(stream.getPosition() - 2); + this.countryString = stream.readString(2); + } + this.language = stream.readUint16(); + if (this.language > 255) { + stream.seek(stream.getPosition() - 2); + this.parseLanguage(stream); + } + this.raw = stream.readUint8Array(this.size - this.hdr_size - 8); + this.value = parseItifData(this.valueType, this.raw); + } +} diff --git a/src/boxes/qt/ilst.ts b/src/boxes/qt/ilst.ts new file mode 100644 index 00000000..4459cce5 --- /dev/null +++ b/src/boxes/qt/ilst.ts @@ -0,0 +1,32 @@ +import { Box } from '#/box'; +import { OK } from '#/constants'; +import { parseOneBox } from '#/parser'; +import type { MultiBufferStream } from '#/buffer'; + +/* + * The QTFF ilst Box typically follows a keys Box within a meta Box. + * https://developer.apple.com/documentation/quicktime-file-format/metadata_item_list_atom + */ + +export class ilstBox extends Box { + static override readonly fourcc = 'ilst' as const; + box_name = 'IlstBox' as const; + + /* Indexed by the index in keys */ + list: Record; + + parse(stream: MultiBufferStream) { + this.list = {}; + let total = this.size - this.hdr_size; + while (total > 0) { + const size = stream.readUint32(); + + /* The index into the keys box */ + const index = stream.readUint32(); + + const res = parseOneBox(stream, false, size - 8); + if (res.code === OK) this.list[index] = res.box; + total -= size; + } + } +} diff --git a/src/boxes/qt/keys.ts b/src/boxes/qt/keys.ts new file mode 100644 index 00000000..a1f2c20d --- /dev/null +++ b/src/boxes/qt/keys.ts @@ -0,0 +1,26 @@ +import { FullBox } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; + +/* + * The QTFF keys Atom is typically in a meta Box. + * https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom + static override readonly fourcc = 'ilst' as const; + * key indexes are 1-based and so we store them in a Object, not an array. + */ +export class keysBox extends FullBox { + static override readonly fourcc = 'keys' as const; + box_name = 'KeysBox' as const; + + count: number; + keys: Record; + + parse(stream: MultiBufferStream) { + this.parseFullHeader(stream); + this.count = stream.readUint32(); + this.keys = {}; + for (let i = 0; i < this.count; i++) { + const len = stream.readUint32(); + this.keys[i + 1] = stream.readString(len - 4); + } + } +}