Skip to content

Parse meta box and related boxes in in QTFF format #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions entries/all-boxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions src/DataStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/boxes/ftyp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box } from '#/box';
import { DataStream } from '#/DataStream';
import type { MultiBufferStream } from '#/buffer';

export class ftypBox extends Box {
Expand All @@ -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 */
Expand Down
4 changes: 3 additions & 1 deletion src/boxes/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -34,7 +35,8 @@ export class metaBox extends FullBox {
dinfs: Array<dinfBox>;

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);
}
Expand Down
112 changes: 112 additions & 0 deletions src/boxes/qt/data.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
32 changes: 32 additions & 0 deletions src/boxes/qt/ilst.ts
Original file line number Diff line number Diff line change
@@ -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<number, Box>;

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;
}
}
}
26 changes: 26 additions & 0 deletions src/boxes/qt/keys.ts
Original file line number Diff line number Diff line change
@@ -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<number, string>;

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);
}
}
}