isobmff-inspector
Version:
Simple ISOBMFF parser, compatible with JavaScript and Node.JS
1,309 lines (1,128 loc) • 37.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.inspectISOBMFF = factory());
})(this, (function () { 'use strict';
/**
* Translate groups of 2 big-endian bytes to Integer (from 0 up to 65535).
* @param {TypedArray} bytes
* @param {Number} off - The offset (from the start of the given array)
* @returns {Number}
*/
function be2toi(bytes, off) {
return (bytes[0 + off] << 8) + bytes[1 + off];
}
/**
* Translate groups of 3 big-endian bytes to Integer.
* @param {TypedArray} bytes
* @param {Number} off - The offset (from the start of the given array)
* @returns {Number}
*/
function be3toi(bytes, off) {
return bytes[0 + off] * 0x0010000 + bytes[1 + off] * 0x0000100 + bytes[2 + off];
}
/**
* Translate groups of 4 big-endian bytes to Integer.
* @param {TypedArray} bytes
* @param {Number} off - The offset (from the start of the given array)
* @returns {Number}
*/
function be4toi(bytes, off) {
return bytes[0 + off] * 0x1000000 + bytes[1 + off] * 0x0010000 + bytes[2 + off] * 0x0000100 + bytes[3 + off];
}
/**
* Translate groups of 4 big-endian bytes to Integer.
* @param {TypedArray} bytes
* @param {Number} off - The offset (from the start of the given array)
* @returns {Number}
*/
function be5toi(bytes, off) {
return bytes[0 + off] * 0x100000000 + bytes[1 + off] * 0x001000000 + bytes[2 + off] * 0x000010000 + bytes[3 + off] * 0x000000100 + bytes[4 + off];
}
/**
* Translate groups of 8 big-endian bytes to Integer.
* @param {TypedArray} bytes
* @param {Number} off - The offset (from the start of the given array)
* @returns {Number}
*/
function be8toi(bytes, off) {
return (bytes[0 + off] * 0x1000000 + bytes[1 + off] * 0x0010000 + bytes[2 + off] * 0x0000100 + bytes[3 + off]) * 0x100000000 + bytes[4 + off] * 0x1000000 + bytes[5 + off] * 0x0010000 + bytes[6 + off] * 0x0000100 + bytes[7 + off];
}
function bytesToHex(uint8arr, off, nbBytes) {
if (!uint8arr) {
return "";
}
var arr = uint8arr.slice(off, nbBytes + off);
var hexStr = "";
for (var i = 0; i < arr.length; i++) {
var hex = (arr[i] & 0xff).toString(16);
hex = hex.length === 1 ? "0" + hex : hex;
hexStr += hex;
}
return hexStr.toUpperCase();
} // XXX TODO test that
function betoa(uint8arr, off, nbBytes) {
if (!uint8arr) {
return "";
}
var arr = uint8arr.slice(off, nbBytes + off);
return String.fromCharCode.apply(String, arr);
}
/**
* Create object allowing to easily parse an ISOBMFF box.
*
* The BufferReader saves in its state the current offset after each method
* call, allowing to easily parse contiguous bytes in box parsers.
*
* @param {Uint8Array} buffer
* @returns {Object}
*/
function createBufferReader(buffer) {
var currentOffset = 0;
return {
/**
* Returns the following byte, as a number between 0 and 255.
* @returns {number}
*/
getNextByte: function getNextByte() {
this.getNextBytes(1);
},
/**
* Returns the N next bytes, as an Uint8Array
* @param {number} nb
* @returns {Uint8Array}
*/
getNextBytes: function getNextBytes(nb) {
if (this.getRemainingLength() < nb) {
return;
}
currentOffset += nb;
return buffer.slice(0, nb);
},
/**
* Returns the N next bytes, as a single number.
*
* /!\ only work for now for 1, 2, 3, 4, 5 or 8 bytes.
* TODO Define a more global solution.
*
* /!\ Depending on the size of the number, it may be larger than JS'
* limit.
*
* @param {number} nb
* @returns {number}
*/
bytesToInt: function bytesToInt(nbBytes) {
if (this.getRemainingLength() < nbBytes) {
return;
}
var res;
switch (nbBytes) {
case 1:
res = buffer[currentOffset];
break;
case 2:
res = be2toi(buffer, currentOffset);
break;
case 3:
res = be3toi(buffer, currentOffset);
break;
case 4:
res = be4toi(buffer, currentOffset);
break;
case 5:
res = be5toi(buffer, currentOffset);
break;
case 8:
res = be8toi(buffer, currentOffset);
break;
default:
throw new Error("not implemented yet.");
}
currentOffset += nbBytes;
return res;
},
/**
* Returns the N next bytes into a string of Hexadecimal values.
* @param {number}
* @returns {string}
*/
bytesToHex: function bytesToHex$1(nbBytes) {
if (this.getRemainingLength() < nbBytes) {
return;
}
var res = bytesToHex(buffer, currentOffset, nbBytes);
currentOffset += nbBytes;
return res;
},
/**
* Returns the N next bytes into a string.
* @param {number}
* @returns {string}
*/
bytesToASCII: function bytesToASCII(nbBytes) {
if (this.getRemainingLength() < nbBytes) {
return;
}
var res = betoa(buffer, currentOffset, nbBytes);
currentOffset += nbBytes;
return res;
},
/**
* Returns the total length of the buffer
* @returns {number}
*/
getTotalLength: function getTotalLength() {
return buffer.length;
},
/**
* Returns the length of the buffer which is not yet parsed.
* @returns {number}
*/
getRemainingLength: function getRemainingLength() {
return Math.max(0, buffer.length - currentOffset);
},
/**
* Returns true if this buffer is entirely parsed.
* @returns {boolean}
*/
isFinished: function isFinished() {
return buffer.length <= currentOffset;
}
};
}
var dinf = {
name: "Data Information Box",
description: "Objects that declare the location of the media information in a track.",
container: true
};
var dref = {
name: "Data Reference Box",
description: "",
container: true,
parser: function parser(reader) {
var version = reader.bytesToInt(1);
var flags = reader.bytesToInt(3);
if (version !== 0) {
throw new Error("invalid version");
}
if (flags !== 0) {
throw new Error("invalid flags");
}
var entry_count = reader.bytesToInt(4);
return {
version: version,
flags: flags,
entry_count: entry_count
};
}
};
var edts = {
name: "Edit Box",
description: "Maps the presentation time‐line to the media time‐line as it is stored in the file.",
container: true
};
var free = {
name: "Free Space Box",
description: "This box can be completely ignored"
};
var ftyp = {
name: "File Type Box",
description: "File type and compatibility",
content: [{
/* name: "major brand", */
// optional name
key: "major_brand",
description: "Brand identifier."
}, {
key: "minor_version",
description: "informative integer for the minor version of the major brand"
}, {
key: "compatible_brands",
description: "List of brands"
}],
parser: function parser(reader) {
var len = reader.getTotalLength();
var major_brand = reader.bytesToASCII(4);
var minor_version = reader.bytesToInt(4);
var compatArr = [];
for (var i = 8; i < len; i += 4) {
compatArr.push(reader.bytesToASCII(4));
}
return {
major_brand: major_brand,
minor_version: minor_version,
compatible_brands: compatArr.join(", ")
};
}
};
var hdlr = {
name: "Handler Reference Box",
description: "This box within a Media Box declares media type of the track, " + "and thus the process by which the media‐data in the track is presented",
parser: function parser(r) {
var ret = {
version: r.bytesToInt(1),
flags: r.bytesToInt(3),
pre_defined: r.bytesToInt(4),
handler_type: r.bytesToInt(4),
reserved: [r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4)]
};
var remaining = r.getRemainingLength();
ret.name = "";
while (remaining--) {
ret.name += String.fromCharCode(parseInt(r.bytesToInt(1), 10));
}
return ret;
}
};
// TODO
var iods = {
name: "Initial Object Descriptor Box"
};
var leva = {
name: "Level Assignment Box",
// TODO
parser: function parser(reader) {
var version = reader.bytesToInt(1);
var flags = reader.bytesToInt(3); // ...
return {
version: version,
flags: flags
};
}
};
var mdat = {
name: "Media Data Box",
description: "the content's data"
};
var mdhd = {
name: "Media Header Box",
description: "The media header declares overall information that is " + "media‐independent, and relevant to characteristics of the media in a track.",
parser: function parser(r) {
var version = r.bytesToInt(1);
var flags = r.bytesToInt(3);
var creation_time = r.bytesToInt(version ? 8 : 4);
var modification_time = r.bytesToInt(version ? 8 : 4);
var timescale = r.bytesToInt(4);
var duration = r.bytesToInt(version ? 8 : 4);
var next2Bytes = r.bytesToInt(2);
var pad = next2Bytes >> 15 & 0x01;
var language = [String.fromCharCode((next2Bytes >> 10 & 0x1F) + 0x60), String.fromCharCode((next2Bytes >> 5 & 0x1F) + 0x60), String.fromCharCode((next2Bytes & 0x1F) + 0x60)].join("");
var predifined = r.bytesToInt(2);
return {
version: version,
flags: flags,
creation_time: creation_time,
modification_time: modification_time,
timescale: timescale,
duration: duration,
pad: pad,
language: language,
predifined: predifined
};
}
};
var mdia = {
name: "Track Media Structure",
description: "declare information about the media data within a track.",
container: true
};
var mehd = {
name: "Movie Extends Header Box",
description: "Provides the overall duration, including fragments, of a " + "fragmented movie. If this box is not present, the overall duration must " + "be computed by examining each fragment.",
parser: function parser(reader) {
var version = reader.bytesToInt(1);
if (version > 1) {
throw new Error("invalid version");
}
var flags = reader.bytesToInt(3);
var fragmentDuration = version === 1 ? reader.bytesToInt(8) : reader.bytesToInt(4);
return {
version: version,
flags: flags,
"fragment_duration": fragmentDuration
};
}
};
var mfhd = {
name: "Movie Fragment Header Box",
description: "This box contains just a sequence number (usually starting at 1), as a safety check.",
parser: function parser(r) {
return {
version: r.bytesToInt(1),
flags: r.bytesToInt(3),
sequence_number: r.bytesToInt(4)
};
}
};
var minf = {
name: "Media Information Box",
description: "This box contains all the objects that declare characteristic information of the media in the track.",
container: true
};
var moof = {
name: "Movie Fragment Box",
description: "",
container: true
};
var moov = {
name: "Movie Box",
description: "The movie metadata",
container: true
};
var mvex = {
name: "Movie Extends Box",
container: true
};
var mvhd = {
name: "Movie Header Box",
description: "This box defines overall information which is " + "media‐independent, and relevant to the entire presentation " + "considered as a whole.",
content: [{
name: "version",
description: "mvhd version",
key: "version"
}, {
name: "flags",
description: "mvhd flags",
key: "flags"
}, {
name: "creation_time",
description: "An integer that declares the creation time of the " + "presentation (in seconds since midnight, Jan. 1, 1904, in UTC time)",
key: "creationTime"
}, {
name: "modification_time",
description: "An integer that declares the most recent time the " + "presentation was modified (in seconds since midnight, Jan. 1, 1904, " + "in UTC time)",
key: "modificationTime"
}, {
name: "timescale",
description: "An integer that specifies the time‐scale for the entire " + "presentation; this is the number of time units that pass in one second. " + "For example, a t me coordinate system that measures time in sixtieths " + "of a second has a time scale of 60.",
key: "timescale"
}, {
name: "duration",
description: "An integer that declares length of the presentation (in the " + "indicated timescale). This property is derived from the presentation’s " + "tracks: the value of this field corresponds to the duration of the " + "longest track in the presentation. If the durat ion cannot be " + "determined then duration is set to all 1s.",
key: "duration"
}, {
name: "rate",
description: "A fixed point 16.16 number that indicates the preferred " + "rate to play the presentation; 1.0 (0x00010000) is normal forward playback ",
key: "rate"
}, {
name: "volume",
description: "A fixed point 8.8 number that indicates the preferred playback " + "volume. 1.0 (0x0100) is full volume.",
key: "volume"
}, {
name: "reserved 1",
description: "Reserved 16 bits",
key: "reserved1"
}, {
name: "reserved 2",
description: "Reserved 2*32 bits",
key: "reserved2"
}, {
name: "matrix",
description: "Provides a transformation matrix for the video; (u,v,w) are " + " restricted here to (0,0,1), hex values (0,0,0x40000000).",
key: "matrix"
}, {
name: "pre-defined",
description: "Pre-defined 32*6 bits.",
key: "predefined"
}, {
name: "next_track_ID",
description: "A non‐zero integer that indicates a value to use for the " + "track ID of the next track to be added to this presentation. " + "Zero is not a valid track ID value. The value of next_track_ID shall " + "be larger than the largest track‐ID in use. If this valu e is equal to " + "all 1s (32‐bit maxint), and a new media track is to be added, then a " + "search must be made in the file for an unused track identifier.",
key: "nextTrackId"
}],
parser: function parser(reader) {
var version = reader.bytesToInt(1);
if (version > 1) {
throw new Error("invalid version");
}
var flags = reader.bytesToInt(3);
var creationTime, modificationTime, timescale, duration;
if (version === 1) {
creationTime = reader.bytesToInt(8);
modificationTime = reader.bytesToInt(8);
timescale = reader.bytesToInt(4);
duration = reader.bytesToInt(8);
} else {
creationTime = reader.bytesToInt(4);
modificationTime = reader.bytesToInt(4);
timescale = reader.bytesToInt(4);
duration = reader.bytesToInt(4);
}
var rate = [reader.bytesToInt(2), reader.bytesToInt(2)].join(".");
var volume = [reader.bytesToInt(1), reader.bytesToInt(1)].join(".");
var reserved1 = reader.bytesToInt(2);
var reserved2 = [reader.bytesToInt(4), reader.bytesToInt(4)];
var matrixArr = [];
for (var i = 0; i < 9; i++) {
matrixArr.push(reader.bytesToInt(4));
}
var predefined = [reader.bytesToInt(4), reader.bytesToInt(4), reader.bytesToInt(4), reader.bytesToInt(4), reader.bytesToInt(4), reader.bytesToInt(4)];
var nextTrackId = reader.bytesToInt(4);
return {
version: version,
flags: flags,
creationTime: creationTime,
modificationTime: modificationTime,
timescale: timescale,
duration: duration,
rate: rate,
volume: volume,
reserved1: reserved1,
reserved2: reserved2,
matrix: matrixArr,
predefined: predefined,
nextTrackId: nextTrackId
};
}
};
var pdin = {
name: "Progressive Download Information Box",
description: "",
content: [{
name: "version",
description: "pdin version",
key: "version"
}, {
name: "flags",
description: "pdin flags",
key: "flags"
}, {
name: "rate",
description: "Download rate expressed in bytes/second",
key: "rate"
}, {
name: "initial_delay",
description: "Suggested delay to use when playing the file, such " + "that if download continues at the given rate, all data within " + "the file will arrive in time for its use and playback should " + "not need to stall.",
key: "delay"
}],
parser: function parser(reader) {
var version = reader.bytesToInt(1);
if (version !== 0) {
throw new Error("invalid version");
}
return {
version: version,
flags: reader.bytesToInt(3),
rate: reader.bytesToInt(4),
delay: reader.bytesToInt(4)
};
}
};
var SYSTEM_IDS = {
"1077EFECC0B24D02ACE33C1E52E2FB4B": "cenc",
"1F83E1E86EE94F0DBA2F5EC4E3ED1A66": "SecureMedia",
"35BF197B530E42D78B651B4BF415070F": "DivX DRM",
"45D481CB8FE049C0ADA9AB2D2455B2F2": "CoreCrypt",
"5E629AF538DA4063897797FFBD9902D4": "Marlin",
"616C7469636173742D50726F74656374": "AltiProtect",
"644FE7B5260F4FAD949A0762FFB054B4": "CMLA",
"69F908AF481646EA910CCD5DCCCB0A3A": "Marlin",
"6A99532D869F59229A91113AB7B1E2F3": "MobiDRM",
"80A6BE7E14484C379E70D5AEBE04C8D2": "Irdeto",
"94CE86FB07FF4F43ADB893D2FA968CA2": "FairPlay",
"992C46E6C4374899B6A050FA91AD0E39": "SteelKnot",
"9A04F07998404286AB92E65BE0885F95": "PlayReady",
"9A27DD82FDE247258CBC4234AA06EC09": "Verimatrix VCAS",
"A68129D3575B4F1A9CBA3223846CF7C3": "VideoGuard Everywhere",
"ADB41C242DBF4A6D958B4457C0D27B95": "Nagra",
"B4413586C58CFFB094A5D4896C1AF6C3": "Viaccess-Orca",
"DCF4E3E362F158187BA60A6FE33FF3DD": "DigiCAP",
"E2719D58A985B3C9781AB030AF78D30E": "ClearKey",
"EDEF8BA979D64ACEA3C827DCD51D21ED": "Widevine",
"F239E769EFA348509C16A903C6932EFB": "PrimeTime"
};
var pssh = {
name: "Protection System Specific Header",
description: "",
parser: function parser(reader) {
var ret = {};
ret.version = reader.bytesToInt(1);
if (ret.version > 1) {
throw new Error("invalid version");
}
ret.flags = reader.bytesToInt(3);
ret.systemID = reader.bytesToHex(16);
var systemIDName = SYSTEM_IDS[ret.systemID];
if (systemIDName) {
ret.systemID += " (".concat(systemIDName, ")");
}
if (ret.version === 1) {
ret.KID_count = reader.bytesToInt(4);
ret.KIDs = [];
var i = ret.KID_count;
while (i--) {
ret.KIDs.push([reader.bytesToHex(16)]);
}
}
ret.data_length = reader.bytesToInt(4);
ret.data = reader.bytesToHex(ret.data_length);
return ret;
}
};
var saio = {
name: "Sample Auxiliary Information Offsets",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
if (ret.flags == 1) {
ret.aux_info_type = r.bytesToInt(4);
ret.aux_info_type_parameter = r.bytesToInt(4);
}
ret.entry_count = r.bytesToInt(4);
ret.offset = [];
var i = ret.entry_count;
while (i--) {
ret.offset.push(r.bytesToInt(ret.version == 0 ? 4 : 8));
}
return ret;
}
};
var saiz = {
name: "Sample Auxiliary Information Sizes",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
if (ret.flags == 1) {
ret.aux_info_type = r.bytesToInt(4);
ret.aux_info_type_parameter = r.bytesToInt(4);
}
ret.default_sample_info_size = r.bytesToInt(1);
ret.sample_count = r.bytesToInt(4);
if (ret.default_sample_info_size == 0) {
ret.sample_info_size = [];
var i = ret.sample_count;
while (i--) {
ret.sample_info_size.push(r.bytesToInt(1));
}
}
return ret;
}
};
var sdtp = {
name: "Independent and Disposable Samples Box",
description: "",
parser: function parser(r) {
var ret = {
version: r.bytesToInt(1),
flags: r.bytesToInt(3)
};
var remaining = r.getRemainingLength();
var i = remaining;
ret.samples = [];
while (i--) {
var _byte = r.bytesToInt(1);
ret.samples.push({
is_leading: _byte >> 6 & 0x03,
sample_depends_on: _byte >> 4 & 0x03,
sample_is_depended_on: _byte >> 2 & 0x03,
sample_has_redundancy: _byte & 0x03
});
}
return ret;
}
};
var sidx = {
name: "Segment Index Box",
description: "Index of the media stream",
parser: function parser(r) {
var version = r.bytesToInt(1);
var flags = r.bytesToInt(3);
var reference_id = r.bytesToInt(4);
var timescale = r.bytesToInt(4);
var earliest_presentation_time = r.bytesToInt(version === 0 ? 4 : 8);
var first_offset = r.bytesToInt(version === 0 ? 4 : 8);
var reserved = r.bytesToInt(2);
var reference_count = r.bytesToInt(2);
var items = [];
var i = reference_count;
while (i--) {
var first4Bytes = r.bytesToInt(4);
var second4Bytes = r.bytesToInt(4);
var third4Bytes = r.bytesToInt(4);
items.push({
reference_type: first4Bytes >> 31 & 0x01,
referenced_size: first4Bytes & 0x7FFFFFFF,
subsegment_duration: second4Bytes,
starts_with_SAP: third4Bytes >> 31 & 0x01,
SAP_type: third4Bytes >> 28 & 0x07,
SAP_delta_time: third4Bytes & 0x0FFFFFFF
});
}
return {
version: version,
flags: flags,
reference_id: reference_id,
timescale: timescale,
earliest_presentation_time: earliest_presentation_time,
first_offset: first_offset,
reserved: reserved,
reference_count: reference_count,
items: items
};
}
};
var skip = {
name: "Free Space Box",
description: "This box can be completely ignored."
};
var stbl = {
name: "Sample Table",
description: "",
container: true
};
var stco = {
name: "Chunk Offset",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
ret.entry_count = r.bytesToInt(4);
ret.chunk_offsets = [];
var i = ret.entry_count;
while (i--) {
ret.chunk_offsets.push(r.bytesToInt(4));
}
return ret;
}
};
var stsc = {
name: "Sample To Chunk",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
ret.entry_count = r.bytesToInt(4);
ret.entries = [];
var i = ret.entry_count;
while (i--) {
var e = {};
e.first_chunk = r.bytesToInt(4);
e.samples_per_chunk = r.bytesToInt(4);
e.sample_description_index = r.bytesToInt(4);
ret.entries.push(e);
}
return ret;
}
};
var stsd = {
name: "Sample Description",
description: "Information about the coding type used",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
ret.entry_count = r.bytesToInt(4);
return ret;
},
container: true
};
var stsz = {
name: "Sample Size",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
ret.sample_size = r.bytesToInt(4);
ret.sample_count = r.bytesToInt(4);
if (ret.sample_size == 0) {
ret.entries = [];
var i = ret.sample_count;
while (i--) {
ret.entries.push(r.bytesToInt(4));
}
}
return ret;
}
};
var stts = {
name: "Decoding Time to Sample",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
ret.entry_count = r.bytesToInt(4);
ret.entries = [];
var i = ret.entry_count;
while (i--) {
var e = {};
e.sample_count = r.bytesToInt(4);
e.sample_delta = r.bytesToInt(4);
ret.entries.push(e);
}
return ret;
}
};
var styp = {
name: "Segment Type Box",
description: "",
content: ftyp.content,
parser: ftyp.parser
};
var tfdt = {
name: "Track Fragment Decode Time",
description: "The absolute decode time, measured on the media timeline, of " + "the first sample in decode order in the track fragment",
parser: function parser(r) {
var version = r.bytesToInt(1);
return {
version: version,
flags: r.bytesToInt(3),
baseMediaDecodeTime: r.bytesToInt(version ? 8 : 4)
};
}
};
var tfhd = {
name: "Track Fragment Header Box",
description: "",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
var flags = r.bytesToInt(3);
var hasBaseDataOffset = flags & 0x000001;
var hasSampleDescriptionIndex = flags & 0x000002;
var hasDefaultSampleDuration = flags & 0x000008;
var hasDefaultSampleSize = flags & 0x000010;
var hasDefaultSampleFlags = flags & 0x000020;
var durationIsEmpty = flags & 0x010000;
var defaultBaseIsMOOF = flags & 0x020000;
ret.flags = {
"base-data-offset-present": !!hasBaseDataOffset,
"sample-description-index-present": !!hasSampleDescriptionIndex,
"default-sample-duration-present": !!hasDefaultSampleDuration,
"default-sample-size-present": !!hasDefaultSampleSize,
"default-sample-flags-present": !!hasDefaultSampleFlags,
"duration-is-empty": !!durationIsEmpty,
"default-base-is-moof": !!defaultBaseIsMOOF
};
ret.track_ID = r.bytesToInt(4);
if (hasBaseDataOffset) {
ret.base_data_offset = r.bytesToInt(8);
}
if (hasSampleDescriptionIndex) {
ret.sample_description_index = r.bytesToInt(4);
}
if (hasDefaultSampleDuration) {
ret.default_sample_duration = r.bytesToInt(4);
}
if (hasDefaultSampleSize) {
ret.default_sample_size = r.bytesToInt(4);
}
if (hasDefaultSampleFlags) {
ret.default_sample_flags = r.bytesToInt(4);
}
return ret;
}
};
var tkhd = {
name: "Track Header Box",
description: "Characteristics of a single track.",
parser: function parser(r) {
var version = r.bytesToInt(1);
return {
version: version,
flags: r.bytesToInt(3),
creation_time: r.bytesToInt(version ? 8 : 4),
modification_time: r.bytesToInt(version ? 8 : 4),
track_ID: r.bytesToInt(4),
reserved1: r.bytesToInt(4),
duration: r.bytesToInt(version ? 8 : 4),
reserved2: [r.bytesToInt(4), r.bytesToInt(4)],
// TODO template? signed?
layer: r.bytesToInt(2),
alternate_group: r.bytesToInt(2),
volume: [r.bytesToInt(1), r.bytesToInt(1)].join("."),
reserved3: r.bytesToInt(2),
matrix: [r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4), r.bytesToInt(4)],
width: [r.bytesToInt(2), r.bytesToInt(2)],
height: [r.bytesToInt(2), r.bytesToInt(2)]
};
}
};
var traf = {
name: "Track Fragment Box",
description: "",
container: true
};
var trak = {
name: "Track Box",
description: "Container box for a single track of a presentation. " + "A presentation consists of one or more tracks. Each track is independent " + "of the other tracks in the presentation and carries its own temporal and " + "spatial information. Each track will contain its associated Media Box.",
container: true
};
var trex = {
name: "Track Extends Box",
description: "sets up default values used by the movie fragments. " + "By setting defaults in this way, space and complexity can be saved " + "in each Track Fragment Box",
parser: function parser(reader) {
return {
version: reader.bytesToInt(1),
flags: reader.bytesToInt(3),
"track_id": reader.bytesToInt(4),
"default_sample_description_index": reader.bytesToInt(4),
"default_sample_duration": reader.bytesToInt(4),
"default_sample_size": reader.bytesToInt(4),
"default_sample_flags": reader.bytesToInt(4)
};
}
};
var trun = {
name: "Track Fragment Run Box",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
var flags = r.bytesToInt(3);
var hasDataOffset = flags & 0x000001;
var hasFirstSampleFlags = flags & 0x000004;
var hasSampleDuration = flags & 0x000100;
var hasSampleSize = flags & 0x000200;
var hasSampleFlags = flags & 0x000400;
var hasSampleCompositionOffset = flags & 0x000800;
ret.flags = {
"data-offset-present": !!hasDataOffset,
"first-sample-flags-present": !!hasFirstSampleFlags,
"sample-duration-present": !!hasSampleDuration,
"sample-size-present": !!hasSampleSize,
"sample-flags-present": !!hasSampleFlags,
"sample-composition-time-offset-present": !!hasSampleCompositionOffset
};
ret.sample_count = r.bytesToInt(4); // two's complement
if (hasDataOffset) {
ret.data_offset = ~~r.bytesToInt(4);
}
if (hasFirstSampleFlags) {
ret.first_sample_flags = r.bytesToInt(4);
}
var i = ret.sample_count;
ret.samples = [];
while (i--) {
var sample = {};
if (hasSampleDuration) {
sample.sample_duration = r.bytesToInt(4);
}
if (hasSampleSize) {
sample.sample_size = r.bytesToInt(4);
}
if (hasSampleFlags) {
sample.sample_flags = r.bytesToInt(4);
}
if (hasSampleCompositionOffset) {
sample.sample_composition_time_offset = ret.version === 0 ? r.bytesToInt(4) : ~~r.bytesToInt(4);
}
ret.samples.push(sample);
}
return ret;
}
};
var url_ = {
name: "Data Entry Url Box",
description: "declare the location(s) of the media data used within the presentation.",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
var remaining = r.getRemainingLength();
if (remaining) {
ret.location = String.fromCharCode.apply(String, r.bytesToInt(r.getRemainingLength()));
}
return ret;
}
};
var urn_ = {
name: "Data Entry Url Box",
description: "declare the location(s) of the media data used within the presentation.",
parser: function parser(r) {
var ret = {};
ret.version = r.bytesToInt(1);
ret.flags = r.bytesToInt(3);
var remaining = r.getRemainingLength(); // TODO Check NULL-terminated stream for name+location
// might also check flags for that
if (remaining) {
ret.name = String.fromCharCode.apply(String, r.bytesToInt(r.getRemainingLength()));
}
return ret;
}
};
var uuid = {
name: "User-defined Box",
description: "Custom box. Those are not yet parsed here."
};
var vmhd = {
name: "Video Media Header",
description: "The video media header contains general presentation " + "information, independent of the coding, for video media.",
parser: function parser(reader) {
var version = reader.bytesToInt(1);
var flags = reader.bytesToInt(3);
if (version !== 0) {
throw new Error("invalid version");
}
if (flags !== 1) {
throw new Error("invalid flags");
} // TODO template?
var graphicsmode = reader.bytesToInt(2);
var opcolor = [reader.bytesToInt(2), reader.bytesToInt(2), reader.bytesToInt(2)];
return {
version: version,
flags: flags,
graphicsmode: graphicsmode,
opcolor: opcolor
};
}
};
var definitions = {
dinf: dinf,
dref: dref,
edts: edts,
free: free,
ftyp: ftyp,
hdlr: hdlr,
iods: iods,
leva: leva,
mdat: mdat,
mdhd: mdhd,
mdia: mdia,
mehd: mehd,
mfhd: mfhd,
minf: minf,
moof: moof,
moov: moov,
mvex: mvex,
mvhd: mvhd,
pdin: pdin,
pssh: pssh,
saio: saio,
saiz: saiz,
sdtp: sdtp,
sidx: sidx,
skip: skip,
stbl: stbl,
stco: stco,
stsc: stsc,
stsd: stsd,
stsz: stsz,
stts: stts,
styp: styp,
tfdt: tfdt,
tfhd: tfhd,
tkhd: tkhd,
traf: traf,
trak: trak,
trex: trex,
trun: trun,
"url ": url_,
"urn ": urn_,
uuid: uuid,
vmhd: vmhd
};
/**
* Parse recursively ISOBMFF Uint8Array.
* @param {Uint8Array} arr
* @returns {Array.<Object>}
*/
function recursiveParseBoxes(arr) {
var i = 0;
var returnedArray = [];
var _loop = function _loop() {
var currentOffset = i;
var size = be4toi(arr, currentOffset);
currentOffset += 4;
if (size === 1) {
size = be8toi(arr, currentOffset);
currentOffset += 8;
} else if (size === 0) {
size = arr.length - i;
}
var name = betoa(arr, currentOffset, 4);
currentOffset += 4;
var atomObject = {
alias: name,
size: size,
values: []
};
if (name === "uuid") {
var subtype = [];
var j = 16;
while (j--) {
subtype.push(arr[currentOffset]);
currentOffset += 1;
}
atomObject.subtype = subtype;
}
returnedArray.push(atomObject);
if (definitions[name]) {
var config = definitions[name];
var contentInfos = config.content ? config.content.reduce(function (acc, el) {
acc[el.key] = {
name: el.name || "",
description: el.description | ""
};
return acc;
}, {}) : {
name: "",
description: ""
};
atomObject.name = config.name || "";
atomObject.description = config.description || "";
var hasChildren = !!config.container;
var content = arr.slice(currentOffset, size + i);
var contentForChildren;
if (typeof config.parser === "function") {
var parserReader = createBufferReader(content);
var result = {};
try {
result = config.parser(parserReader);
} catch (e) {
console.warn("impossible to parse \"".concat(name, "\" box."), e);
}
if (hasChildren) {
var remaining = parserReader.getRemainingLength();
contentForChildren = content.slice(content.length - remaining);
} else if (!parserReader.isFinished()) {
console.warn("not everything has been parsed for box: " + name + ". Missing", parserReader.getRemainingLength(), "bytes.");
}
delete result.__data__;
Object.keys(result).forEach(function (key) {
var infos = contentInfos[key] || {};
if (!infos.name) {
infos.name = key;
}
atomObject.values.push(Object.assign({
value: result[key]
}, infos));
});
}
if (hasChildren) {
var childrenResult = parseBoxes(contentForChildren || content);
atomObject.children = childrenResult;
}
}
i += size;
};
while (i < arr.length) {
_loop();
}
return returnedArray;
}
/**
* Parse ISOBMFF file and translate it into a more useful array containing
* "atom objects".
* @param {ArrayBuffer|Uint8Array} arr
* @returns {Array.<Object>}
*/
function parseBoxes(arr) {
if (arr instanceof Uint8Array) {
return recursiveParseBoxes(arr);
}
if (arr instanceof ArrayBuffer) {
return recursiveParseBoxes(new Uint8Array(arr));
}
if (arr.buffer instanceof ArrayBuffer) {
return recursiveParseBoxes(new Uint8Array(arr.buffer));
}
throw new Error("Unrecognized format. " + "Please give an ArrayBuffer or TypedArray instead.");
}
return parseBoxes;
}));