diff --git a/src/DataStream.js b/src/DataStream.js
index c367cbd7..11d592e9 100644
--- a/src/DataStream.js
+++ b/src/DataStream.js
@@ -20,6 +20,9 @@ var DataStream = function(arrayBuffer, byteOffset, endianness) {
}
this.position = 0;
this.endianness = endianness == null ? DataStream.LITTLE_ENDIAN : endianness;
+
+ // Behavior flags set by parsers
+ this.behavior = 0;
};
DataStream.prototype = {};
diff --git a/src/box.js b/src/box.js
index 9c65bb68..47acd063 100644
--- a/src/box.js
+++ b/src/box.js
@@ -7,6 +7,9 @@ var BoxParser = {
ERR_NOT_ENOUGH_DATA : 0,
OK : 1,
+ // Behavior set when parsing a QTFF file for when same Box/Atom is used differently
+ BEHAVIOR_QTFF: 0x01,
+
// Boxes to be created with default parsing
BASIC_BOXES: [ "mdat", "idat", "free", "skip", "meco", "strk" ],
FULL_BOXES: [ "hmhd", "nmhd", "iods", "xml ", "bxml", "ipro", "mere" ],
diff --git a/src/parsing/data.js b/src/parsing/data.js
new file mode 100644
index 00000000..869a6c44
--- /dev/null
+++ b/src/parsing/data.js
@@ -0,0 +1,99 @@
+/*
+ * itif data types
+ * https://developer.apple.com/documentation/quicktime-file-format/well-known_types
+ */
+var ItifTypes = {
+ 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,
+};
+
+/*
+ * 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 == ItifTypes.UTF8) {
+ return new TextDecoder("utf-8").decode(data);
+ }
+
+ var view = new DataView(data.buffer);
+ if (type == ItifTypes.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 == ItifTypes.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 == ItifTypes.BE_FLOAT32) {
+ return view.getFloat32(0, false);
+ }
+
+ Log.warn("BoxParser", "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
+ */
+BoxParser.createBoxCtor("data", function(stream) {
+ this.type = stream.readUint32();
+ this.country = stream.readUint16();
+ if (this.country > 255) {
+ stream.position -= 2;
+ this.countryString = stream.readString(2);
+ }
+ this.language = stream.readUint16();
+ if (this.language > 255) {
+ stream.position -= 2;
+ this.parseLanguage(stream);
+ }
+ this.raw = stream.readUint8Array(this.size - this.hdr_size - 8);
+ this.value = parseItifData(this.type, this.raw);
+});
+
+if (typeof exports !== 'undefined') {
+ exports.ItifTypes = ItifTypes;
+}
+
diff --git a/src/parsing/ftyp.js b/src/parsing/ftyp.js
index 5864d62b..d9edd3b3 100644
--- a/src/parsing/ftyp.js
+++ b/src/parsing/ftyp.js
@@ -10,5 +10,9 @@ BoxParser.createBoxCtor("ftyp", function(stream) {
toparse -= 4;
i++;
}
+
+ // Certain Boxes/Atoms have different behavior when parsing QTFF files
+ if (this.major_brand.indexOf("qt") == 0)
+ stream.behavior |= BoxParser.BEHAVIOR_QTFF;
});
diff --git a/src/parsing/ilst.js b/src/parsing/ilst.js
new file mode 100644
index 00000000..c0ae76a9
--- /dev/null
+++ b/src/parsing/ilst.js
@@ -0,0 +1,18 @@
+/*
+ * 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
+ */
+BoxParser.createBoxCtor("ilst", function(stream) {
+ var total = this.size - this.hdr_size;
+ this.boxes = { };
+ while (total > 0) {
+ var size = stream.readUint32();
+
+ /* The index into the keys box */
+ var index = stream.readUint32();
+ var res = BoxParser.parseOneBox(stream, false, size - 8);
+ if (res.code == BoxParser.OK)
+ this.boxes[index] = res.box;
+ total -= size;
+ }
+});
diff --git a/src/parsing/keys.js b/src/parsing/keys.js
new file mode 100644
index 00000000..aa406b7f
--- /dev/null
+++ b/src/parsing/keys.js
@@ -0,0 +1,14 @@
+/*
+ * The QTFF keys Atom is typically in a meta Box.
+ * https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom
+ * key indexes are 1-based and so we store them in a Object, not an array..
+ */
+BoxParser.createFullBoxCtor("keys", function(stream) {
+ this.count = stream.readUint32();
+ this.keys = {};
+ for (var i = 0; i < this.count; i++) {
+ var len = stream.readUint32();
+ this.keys[i + 1] = stream.readString(len - 4);
+ }
+});
+
diff --git a/src/parsing/meta.js b/src/parsing/meta.js
index d1bc76b6..a5f9623e 100644
--- a/src/parsing/meta.js
+++ b/src/parsing/meta.js
@@ -1,4 +1,9 @@
-BoxParser.createFullBoxCtor("meta", function(stream) {
+// meta is a FullBox in MPEG-4 and a ContainerBox in QTFF
+BoxParser.createContainerBoxCtor("meta", function(stream) {
this.boxes = [];
+
+ // The QTFF "meta" box does not have a flags/version header
+ if (!(stream.behavior & BoxParser.BEHAVIOR_QTFF))
+ BoxParser.FullBox.prototype.parseFullHeader.call(this, stream);
BoxParser.ContainerBox.prototype.parse.call(this, stream);
});
diff --git a/test/qunit-qtff.js b/test/qunit-qtff.js
new file mode 100644
index 00000000..92ae8730
--- /dev/null
+++ b/test/qunit-qtff.js
@@ -0,0 +1,102 @@
+// QTFF .MOV file format tests
+QUnit.module("QTFF");
+
+// Creates new copy of object with only the property keys also in template
+function prunedObject(object, template) {
+ var pruned = { };
+ for (var key in template) {
+ if (key in object)
+ pruned[key] = object[key];
+ }
+ return pruned;
+}
+
+QUnit.asyncTest( "QTFF meta", function(assert) {
+ var timeout = window.setTimeout(function() { assert.ok(false, "Timeout"); QUnit.start(); }, TIMEOUT_MS);
+ var mp4boxfile = MP4Box.createFile();
+ mp4boxfile.onReady = function(info) {
+ window.clearTimeout(timeout);
+ assert.ok(true, "moov found!" );
+
+ var metas = mp4boxfile.getBoxes("meta");
+ assert.ok(metas, "metas is not null");
+ assert.strictEqual(metas.length, 2, "two metas fonud");
+ assert.strictEqual(metas[0].type, "meta", "Correct meta box");
+ assert.strictEqual(metas[1].type, "meta", "Correct meta box");
+
+ /* A few fields in the first meta */
+ assert.strictEqual(metas[0].boxes[0].type, "hdlr", "Correct hdlr box");
+ assert.strictEqual(metas[0].boxes[1].type, "keys", "Correct keys box");
+ assert.strictEqual(metas[0].boxes[2].type, "ilst", "Correct ilst box");
+
+ assert.strictEqual(metas[0].boxes[1].count, 2);
+ [
+ "mdtacom.apple.quicktime.camera.lens_model",
+ "mdtacom.apple.quicktime.camera.focal_length.35mm_equivalent"
+ ].map(function(val, i) {
+ assert.strictEqual(metas[0].boxes[1].keys[i + 1], val, "key correct: " + (i + 1));
+ });
+
+ assert.strictEqual(Object.keys(metas[0].boxes[2].boxes).length, 2);
+ [
+ {
+ type: ItifTypes.UTF8, value: "iPhone 14 Pro back camera 9mm f/2.8",
+ country: 17477, countryString: "DE", language: 5575, languageString: "eng",
+ },
+ {
+ type: ItifTypes.BE_SIGNED_INT, value: 75,
+ country: 17477, countryString: "DE", language: 5575, languageString: "eng",
+ }
+ ].map(function(val, i) {
+ var box = prunedObject(metas[0].boxes[2].boxes[i + 1], val);
+ assert.deepEqual(box, val, "ilst values correct:" + (i + 1));
+ });
+
+ /* This file has a second meta with lots more data */
+ assert.strictEqual(metas[1].boxes[0].type, "hdlr", "Correct hdlr box");
+ assert.strictEqual(metas[1].boxes[1].type, "keys", "Correct keys box");
+ assert.strictEqual(metas[1].boxes[2].type, "ilst", "Correct ilst box");
+
+ assert.strictEqual(metas[1].boxes[1].count, 11, "Correct number of keys");
+ [
+ "mdtacom.apple.quicktime.location.accuracy.horizontal",
+ "mdtacom.apple.quicktime.live-photo.auto",
+ "mdtacom.apple.quicktime.full-frame-rate-playback-intent",
+ "mdtacom.apple.quicktime.live-photo.vitality-score",
+ "mdtacom.apple.quicktime.live-photo.vitality-scoring-version",
+ "mdtacom.apple.quicktime.location.ISO6709",
+ "mdtacom.apple.quicktime.make",
+ "mdtacom.apple.quicktime.model",
+ "mdtacom.apple.quicktime.software",
+ "mdtacom.apple.quicktime.creationdate",
+ "mdtacom.apple.quicktime.content.identifier",
+ ].map(function(val, i) {
+ assert.strictEqual(metas[1].boxes[1].keys[i + 1], val, "key correct: " + (i + 1));
+ });
+
+ assert.strictEqual(Object.keys(metas[1].boxes[2].boxes).length, 11);
+ [
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "7.176223" },
+ { type: ItifTypes.BE_UNSIGNED_INT, country: 0, language: 0, value: 1 },
+ { type: ItifTypes.BE_SIGNED_INT, country: 0, language: 0, value: 1n },
+ { type: ItifTypes.BE_FLOAT32, country: 0, language: 0, value: 0.9398496150970459 },
+ { type: ItifTypes.BE_SIGNED_INT, country: 0, language: 0, value: 4n },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "+32.0532+076.7050+2083.109/" },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "Apple" },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "iPhone 14 Pro" },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "18.0" },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "2024-10-08T14:52:09+0530" },
+ { type: ItifTypes.UTF8, country: 0, language: 0, value: "C83404C7-130C-474E-B5E7-9865353C2DE7" },
+ ].map(function(val, i) {
+ var box = prunedObject(metas[1].boxes[2].boxes[i + 1], val);
+ assert.deepEqual(box, val, "ilst values correct: " + (i + 1));
+ });
+ }
+
+ var url = mediaTestBaseUrl + 'mov/iphone.mov';
+ getFile(url, function (buffer) {
+ mp4boxfile.appendBuffer(buffer);
+ QUnit.start();
+ });
+});
+
diff --git a/test/qunit.html b/test/qunit.html
index 084662b9..cadbeb1e 100644
--- a/test/qunit.html
+++ b/test/qunit.html
@@ -12,6 +12,7 @@
+