Skip to content

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

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

Closed
wants to merge 2 commits into from
Closed
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 src/DataStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand Down
3 changes: 3 additions & 0 deletions src/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" ],
Expand Down
99 changes: 99 additions & 0 deletions src/parsing/data.js
Original file line number Diff line number Diff line change
@@ -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;
}

4 changes: 4 additions & 0 deletions src/parsing/ftyp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

18 changes: 18 additions & 0 deletions src/parsing/ilst.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
14 changes: 14 additions & 0 deletions src/parsing/keys.js
Original file line number Diff line number Diff line change
@@ -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);
}
});

7 changes: 6 additions & 1 deletion src/parsing/meta.js
Original file line number Diff line number Diff line change
@@ -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);
});
102 changes: 102 additions & 0 deletions test/qunit-qtff.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

1 change: 1 addition & 0 deletions test/qunit.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<script src="qunit-box-data.js"></script>
<script src="qunit-tests.js"></script>
<script src="qunit-mse-tests.js"></script>
<script src="qunit-qtff.js"></script>
<script src="qunit-isofile-tests.js"></script>
<script src="iso-conformance-files.js"></script>
<script src="qunit-iso-conformance.js"></script>
Expand Down