From: Jer Noble Date: Thu, 9 Feb 2017 23:40:54 +0000 (-0800) Subject: Add support for many new atom types. X-Git-Url: http://id.pley.net/movie_parser.git/commitdiff_plain/b73211b4d80bf99a6cbb870c33a1d1a3afc8fbce?ds=inline;hp=9114b5c6a1b71b9c3007b44d69acf3b886861a4d Add support for many new atom types. --- diff --git a/Atom.js b/Atom.js index 8a73803..a3ff73b 100644 --- a/Atom.js +++ b/Atom.js @@ -1,718 +1,1833 @@ -var Atom = function (buffer, offset) { - this.setDefaults(); - return this.parse(buffer, offset) ? this : null; -}; - -Atom.create = function (buffer, offset) { - // 'offset' is optional. - if (arguments.length < 2) { - offset = 0; +class BitReader { + constructor(buffer, offset) { + this.buffer = buffer; + this.bitPos = offset * 8; } - if (buffer.byteLength - offset < this.minimumSize) - return null; + readOneBit() { + var offset = Math.floor(this.bitPos / 8), + shift = 7 - this.bitPos % 8; + this.bitPos += 1; + return (this.buffer[offset] >> shift) & 1; + } - var typeArrayView = new Uint8Array(buffer, offset + 4, 4); - var type = String.fromCharCode.apply(null, typeArrayView); - var atom; - - switch (type) { - case 'ftyp': - return new FileTypeAtom(buffer, offset); - case 'moov': - return new ContainerAtom(buffer, offset, 'Movie Atom'); - case 'trak': - return new ContainerAtom(buffer, offset, 'Track Atom'); - case 'mdia': - return new ContainerAtom(buffer, offset, 'Media Atom'); - case 'minf': - return new ContainerAtom(buffer, offset, 'Media Info Atom'); - case 'mvex': - return new ContainerAtom(buffer, offset, 'Movie Extends Atom'); - case 'stbl': - return new ContainerAtom(buffer, offset); - case 'mvhd': - return new MovieHeaderAtom(buffer, offset); - case 'tkhd': - return new TrackHeaderAtom(buffer, offset); - case 'mdhd': - return new MediaHeaderAtom(buffer, offset); - case 'stss': - return new SyncSampleAtom(buffer, offset); - case 'stts': - return new TimeToSampleAtom(buffer, offset); - case 'stsz': - return new SampleSizeAtom(buffer, offset); - case 'trex': - return new TrackExtendsAtom(buffer, offset); - case 'sinf': - return new ContainerAtom(buffer, offset, 'Protection Scheme Info Atom'); - case 'ipro': - return new ContainerAtom(buffer, offset, 'Item Protection Atom'); - case 'frma': - return new OriginalFormatBox(buffer, offset); - case 'schm': - return new SchemeTypeBox(buffer, offset); - case 'schi': - return new SchemeInfoBox(Buffer, offset); - default: - return new Atom(buffer, offset); + readBits(n) { + var i, value = 0; + for (i = 0; i < n; i += 1) { + value = value << 1 | this.readOneBit(); + } + return value; } -}; -Atom.prototype.super = function (object) { - return Object.getPrototypeOf(object.prototype); -}; + isEnd() { + return Math.floor(this.bitPos / 8) >= this.buffer.length; + } +} -Atom.prototype.setDefaults = function () { - Object.defineProperty(this, "is64bit", { +class Atom { + constructor(parent) { + Object.defineProperty(this, "is64bit", { value: false, writable: true, enumerable: false, configurable: true, }); - Object.defineProperty(this, "minimumSize", { + Object.defineProperty(this, "minimumSize", { value: 8, writable: true, enumerable: false, configurable: true, }); - Object.defineProperty(this, "parent", { + Object.defineProperty(this, "parent", { value: null, writable: true, enumerable: false, configurable: true, }); - Object.defineProperty(this, "description", { + Object.defineProperty(this, "description", { value: "Undifferentiated Atom", writable: true, enumerable: false, configurable: true, }); - this.offset = 0; - this.size = 0; - this.type = ''; -}; -Atom.prototype.parse = function (buffer, offset) { - // 'offset' is optional. - if (typeof(offset) == 'undefined') - offset = 0; + this.offset = 0; + this.size = 0; + this.type = ''; + this.parent = parent; + + return this; + }; + + static create(buffer, offset, parent) { + // 'offset' is optional. + if (arguments.length < 2) { + offset = 0; + } + + var type = this.getType(buffer, offset); + var atom; - this.offset = offset; + if (typeof(Atom.constructorMap[type]) == 'undefined') + atom = new Atom(parent); + else + atom = new Atom.constructorMap[type](parent); + atom.parse(buffer, offset); + return atom; + }; - if (buffer.byteLength - offset < this.minimumSize) - throw 'Buffer not long enough'; + static getType(buffer, offset) { + // 'offset' is optional. + if (arguments.length < 2) { + offset = 0; + } - var view = new DataView(buffer, offset, 4); - var headerOffset = 0; + if (buffer.byteLength - offset < this.minimumSize) + return null; - this.size = view.getUint32(0); - headerOffset += 4; + var view = new DataView(buffer, offset, 4); + var size = view.getUint32(0); + if (size == 1) { + var upper = view.getUint32(8); + var lower = view.getUint32(12); + size = (upper << 32) + lower; + } - var typeArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.type = String.fromCharCode.apply(null, typeArrayView); - headerOffset += 4; + if (!size || buffer.byteLength < offset + size) + return null; + + var typeArrayView = new Uint8Array(buffer, offset + 4, 4); + return String.fromCharCode.apply(null, typeArrayView); + }; + + + parse(buffer, offset) { + // 'offset' is optional. + if (typeof(offset) == 'undefined') + offset = 0; + + this.offset = offset; - if (this.size == 1) { - this.is64bit = true; if (buffer.byteLength - offset < 8) - throw 'Malformed extended size field'; - - // NOTE: JavaScript can only represent up to 2^53 as precise integer. - // This calculation may result in incorrect values. - var view = new DataView(buffer, offset + headerOffset, 8); - var upper = view.getUint32(0); - var lower = view.getUint32(4); - this.size = (upper << 32) + lower; - headerOffset += 8; - } + throw 'Buffer not long enough'; - return headerOffset; -}; + var view = new DataView(buffer, offset, 4); + var headerOffset = 0; + + this.size = view.getUint32(0); + headerOffset += 4; + + var typeArrayView = new Uint8Array(buffer, offset + headerOffset, 4); + this.type = String.fromCharCode.apply(null, typeArrayView); + headerOffset += 4; + + if (this.size == 1) { + this.is64bit = true; + if (buffer.byteLength - offset < 8) + throw 'Malformed extended size field'; + + // NOTE: JavaScript can only represent up to 2^53 as precise integer. + // This calculation may result in incorrect values. + view = new DataView(buffer, offset + headerOffset, 8); + var upper = view.getUint32(0); + var lower = view.getUint32(4); + this.size = (upper << 32) + lower; + headerOffset += 8; + } + + if (this.type === 'uuid') { + var extendedTypeArray = new Uint8Array(buffer, offset + headerOffset, 16); + this.uuid = String.fromCharCode.apply(null, extendedTypeArray); + headerOffset += 16; + } + + return headerOffset; + }; + + getAtomByType(type) { + if (typeof(this.childAtoms) == 'undefined') + return null; + + // Bredth first + var result = this.childAtoms.find(function(atom) { + return atom.type == type; + }); + if (result) + return result; + + for (var i = 0; i < this.childAtoms.length; ++i) { + var atom = this.childAtoms[i].getAtomByType(type); + if (atom) + return atom; + } -Atom.prototype.getAtomByType = function (type) { - if (typeof(this.childAtoms) == 'undefined') return null; + }; - // Bredth first - for (var index in this.childAtoms) { - if (this.childAtoms[index].type == type) - return this.childAtoms[index]; - } + getAtomsByType(type) { + if (typeof(this.childAtoms) == 'undefined') + return []; - var result = null; - for (var index in this.childAtoms) { - if (result = this.childAtoms[index].getAtomByType(type)) - break; - } - return result; + // Bredth first + var result = this.childAtoms.filter(function(atom) { + return atom.type === type; + }); + + this.childAtoms.forEach(function(atom) { + result = result.concat(atom.getAtomsByType(type)); + }); + + return result; + }; }; -Atom.prototype.getAtomsByType = function (type) { - if (typeof(this.childAtoms) == 'undefined') - return []; +Atom.constructorMap = { }; - var result = []; +class FileTypeAtom extends Atom { + constructor(parent) { + super(parent); - // Bredth first - for (var index in this.childAtoms) { - if (this.childAtoms[index].type == type) - result.push(this.childAtoms[index]); + this.description = "File Type Atom"; + this.minimumSize = 16; + this.brand = ""; + this.version = 0; + this.compatible_brands = []; } - for (var index in this.childAtoms) - result = result.concat(this.childAtoms[index].getAtomsByType(type)); + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset, this.size); - return result; + var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); + this.brand = String.fromCharCode.apply(null, brandArrayView); + headerOffset += 4; + + this.version = view.getUint32(headerOffset); + headerOffset += 4; + + while (headerOffset < this.size - 4) { + brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); + this.compatible_brands.push(String.fromCharCode.apply(null, brandArrayView)); + headerOffset += 4; + } + + return headerOffset; + }; }; -var FileTypeAtom = function (buffer, offset) { - this.super(FileTypeAtom).constructor.call(this, buffer, offset); -} +Atom.constructorMap['ftyp'] = FileTypeAtom.bind(null); + +class ContainerAtom extends Atom { + constructor(description, parent) { + super(parent); + this.description = description; + this.childAtoms = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset, this); + while (headerOffset < this.size) { + var childAtom = Atom.create(buffer, offset + headerOffset, this); + if (!childAtom) + break; + headerOffset += childAtom.size; + this.childAtoms.push(childAtom); + } + return headerOffset; + }; +}; -FileTypeAtom.prototype = Object.create(Atom.prototype); +Atom.constructorMap['moov'] = ContainerAtom.bind(null, 'Movie Atom'); +Atom.constructorMap['trak'] = ContainerAtom.bind(null, 'Track Atom'); +Atom.constructorMap['mdia'] = ContainerAtom.bind(null, 'Media Atom'); +Atom.constructorMap['minf'] = ContainerAtom.bind(null, 'Media Info Atom'); +Atom.constructorMap['mvex'] = ContainerAtom.bind(null, 'Movie Extends Atom'); +Atom.constructorMap['sinf'] = ContainerAtom.bind(null, 'Protection Scheme Info Atom'); +Atom.constructorMap['ipro'] = ContainerAtom.bind(null, 'Item Protection Atom'); +Atom.constructorMap['stbl'] = ContainerAtom.bind(null, 'Sample Table Atom'); +Atom.constructorMap['moof'] = ContainerAtom.bind(null, 'Movie Fragment Atom'); +Atom.constructorMap['traf'] = ContainerAtom.bind(null, 'Track Fragment Atom'); +Atom.constructorMap['edts'] = ContainerAtom.bind(null, 'Edit Box'); + +class FullBox extends Atom { + constructor(parent) { + super(parent); + this.version = 0; + this.flags = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.version = view.getUint8(headerOffset); + headerOffset += 1; + + // 'flags' is a 3-byte field, so retrieve from one extra byte and concatenate + this.flags = (view.getUint8(headerOffset) << 8) + view.getUint16(headerOffset + 1); + headerOffset += 3; + + return headerOffset; + }; +}; -FileTypeAtom.prototype.setDefaults = function () { - this.super(FileTypeAtom).setDefaults.call(this); - this.description = "File Type Atom"; - this.minimumSize = 16; - this.brand = ""; - this.version = 0; - this.compatible_brands = []; -} +class MovieHeaderAtom extends FullBox { + constructor(parent) { + super(parent); + this.description = "Movie Header Atom"; + this.creationTime = 0; + this.modificationTime = 0; + this.timeScale = 0; + this.duration = 0; + this.preferredRate = 0.0; + this.preferredVolume = 0.0; + this.movieMatrix = [[]]; + this.previewTime = 0; + this.posterTime = 0; + this.selectionTime = 0; + this.selectionDuration = 0; + this.nextTrackID = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; -FileTypeAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(FileTypeAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset, this.size); + this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; - var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.brand = String.fromCharCode.apply(null, brandArrayView); - headerOffset += 4; + this.timeScale = view.getUint32(headerOffset); + headerOffset += 4; - this.version = view.getUint32(headerOffset); - headerOffset += 4; + this.duration = view.getUint32(headerOffset); + headerOffset += 4; - while (headerOffset < this.size - 4) { - var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.compatible_brands.push(String.fromCharCode.apply(null, brandArrayView)); + this.preferredRate = view.getUint32(headerOffset) / (1 << 16); headerOffset += 4; - } - return true; -} + this.preferredVolume = view.getUint16(headerOffset) / (1 << 8); + headerOffset += 2; -var ContainerAtom = function (buffer, offset, description) { - var atom = this.super(ContainerAtom).constructor.call(this, buffer, offset); - atom.description = description; - return; -} + // Reserved + // Ten bytes reserved for use by Apple. Set to 0. + headerOffset += 10; -ContainerAtom.prototype = Object.create(Atom.prototype); + this.movieMatrix = new Array(3); + // a, b, u: + this.movieMatrix[0] = new Array(3); + this.movieMatrix[0][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[0][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[0][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; -ContainerAtom.prototype.setDefaults = function () { - this.super(ContainerAtom).setDefaults.call(this); - this.childAtoms = []; -} + // c, d, v: + this.movieMatrix[1] = new Array(3); + this.movieMatrix[1][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[1][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[1][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; -ContainerAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(ContainerAtom).parse.call(this, buffer, offset); - while (headerOffset < this.size) { - var childAtom = Atom.create(buffer, offset + headerOffset); - if (!childAtom) - break; - headerOffset += childAtom.size; - this.childAtoms.push(childAtom); - childAtom.parent = this; - } - return headerOffset; -} + // x, y, w: + this.movieMatrix[2] = new Array(3); + this.movieMatrix[2][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[2][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.movieMatrix[2][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; -var VersionFlagsAtom = function (buffer, offset) { - this.super(VersionFlagsAtom).constructor.call(this, buffer, offset); -} + this.previewTime = view.getUint32(headerOffset); + headerOffset += 4; -VersionFlagsAtom.prototype = Object.create(Atom.prototype); + this.previewDuration = view.getUint32(headerOffset); + headerOffset += 4; -VersionFlagsAtom.prototype.setDefaults = function () { - this.super(VersionFlagsAtom).setDefaults.call(this); - this.version = 0; - this.flags = 0; -} + this.posterTime = view.getUint32(headerOffset); + headerOffset += 4; -VersionFlagsAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(VersionFlagsAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + this.selectionTime = view.getUint32(headerOffset); + headerOffset += 4; - this.version = view.getUint8(headerOffset); - headerOffset += 1; + this.selectionDuration = view.getUint32(headerOffset); + headerOffset += 4; - // 'flags' is a 3-byte field, so retrieve from one extra byte and concatenate - this.flags = (view.getUint8(headerOffset) << 8) + view.getUint16(headerOffset + 1); - headerOffset += 3; + this.nextTrackID = view.getUint32(headerOffset); + headerOffset += 4; - return headerOffset; -} + return headerOffset; + }; +}; -var MovieHeaderAtom = function (buffer, offset) { - return this.super(MovieHeaderAtom).constructor.call(this, buffer, offset); -} +Atom.constructorMap['mvhd'] = MovieHeaderAtom.bind(null); -MovieHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); - -MovieHeaderAtom.prototype.setDefaults = function () { - this.super(MovieHeaderAtom).setDefaults.call(this); - this.description = "Movie Header Atom"; - this.creationTime = 0; - this.modificationTime = 0; - this.timeScale = 0; - this.duration = 0; - this.preferredRate = 0.0; - this.preferredVolume = 0.0; - this.movieMatrix = [[]]; - this.previewTime = 0; - this.posterTime = 0; - this.selectionTime = 0; - this.selectionDuration = 0; - this.nextTrackID = 0; -} +class EditListBox extends FullBox { + constructor(parent) { + super(parent); + this.description = "Edit List Box"; + this.edits = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + var count = view.getUint32(headerOffset); + headerOffset += 4; + + for (var index = 0; index < count; ++index) { + var segmentDuration = 0; + var mediaTime = 0; + if (this.version === 1) { + var upper = view.getUint32(headerOffset); + var lower = view.getUint32(headerOffset + 4); + segmentDuration = (upper << 32) + lower; + headerOffset += 8; + + upper = view.getUint32(headerOffset); + lower = view.getUint32(headerOffset + 4); + mediaTime = (upper << 32) + lower; + headerOffset += 8; + } else { + segmentDuration = view.getUint32(headerOffset); + headerOffset += 4; + + mediaTime = view.getUint32(headerOffset); + headerOffset += 4; + } + + var mediaRateInteger = view.getUint16(headerOffset); + headerOffset += 2; + + var mediaRateFraction = view.getUint16(headerOffset); + headerOffset += 2; + + this.edits.push([segmentDuration, mediaTime, mediaRateFraction, mediaRateInteger]); + } + + return headerOffset; + }; +}; -MovieHeaderAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(MovieHeaderAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); +Atom.constructorMap['elst'] = EditListBox.bind(null); + +class TrackHeaderAtom extends FullBox { + constructor(parent) { + super(parent); + + this.description = "Track Header Atom"; + this.creationTime = 0; + this.modificationTime = 0; + this.trackID = 0; + this.duration = 0; + this.layer = 0; + this.alternateGroup = 0; + this.volume = 0.0; + this.trackMatrix = []; + this.width = 0; + this.height = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + this.trackID = view.getUint32(headerOffset); + headerOffset += 4; - this.timeScale = view.getUint32(headerOffset); - headerOffset += 4; + // Reserved + // A 32-bit integer that is reserved for use by Apple. Set this field to 0. + headerOffset += 4; - this.duration = view.getUint32(headerOffset); - headerOffset += 4; + this.duration = view.getUint32(headerOffset); + headerOffset += 4; - this.preferredRate = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; + // Reserved + // An 8-byte value that is reserved for use by Apple. Set this field to 0. + headerOffset += 8; - this.preferredVolume = view.getUint16(headerOffset) / (1 << 8); - headerOffset += 2; + this.layer = view.getUint16(headerOffset); + headerOffset += 2; - // Reserved - // Ten bytes reserved for use by Apple. Set to 0. - headerOffset += 10; - - this.movieMatrix = new Array(3); - // a, b, u: - this.movieMatrix[0] = new Array(3); - this.movieMatrix[0][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[0][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[0][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - // c, d, v: - this.movieMatrix[1] = new Array(3); - this.movieMatrix[1][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[1][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[1][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - // x, y, w: - this.movieMatrix[2] = new Array(3); - this.movieMatrix[2][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[2][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.movieMatrix[2][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - this.previewTime = view.getUint32(headerOffset); - headerOffset += 4; - - this.previewDuration = view.getUint32(headerOffset); - headerOffset += 4; - - this.posterTime = view.getUint32(headerOffset); - headerOffset += 4; - - this.selectionTime = view.getUint32(headerOffset); - headerOffset += 4; - - this.selectionDuration = view.getUint32(headerOffset); - headerOffset += 4; - - this.nextTrackID = view.getUint32(headerOffset); - headerOffset += 4; - - return true; -} + this.alternateGroup = view.getUint16(headerOffset); + headerOffset += 2; -var TrackHeaderAtom = function (buffer, offset) { - this.super(TrackHeaderAtom).constructor.call(this, buffer, offset); -} + this.volume = view.getUint16(headerOffset) / (1 << 8); + headerOffset += 2; -TrackHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); - -TrackHeaderAtom.prototype.setDefaults = function () { - this.super(TrackHeaderAtom).setDefaults.call(this); - - this.description = "Track Header Atom"; - this.creationTime = 0; - this.modificationTime = 0; - this.trackID = 0; - this.duration = 0; - this.layer = 0; - this.alternateGroup = 0; - this.volume = 0.0; - this.trackMatrix = []; - this.width = 0; - this.height = 0; -} + // Reserved + // A 16-bit integer that is reserved for use by Apple. Set this field to 0. + headerOffset += 2; -TrackHeaderAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(TrackHeaderAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + this.trackMatrix = new Array(3); + // a, b, u: + this.trackMatrix[0] = new Array(3); + this.trackMatrix[0][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[0][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[0][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + // c, d, v: + this.trackMatrix[1] = new Array(3); + this.trackMatrix[1][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[1][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[1][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + // x, y, w: + this.trackMatrix[2] = new Array(3); + this.trackMatrix[2][0] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[2][1] = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + this.trackMatrix[2][2] = view.getUint32(headerOffset) / (1 << 30); + headerOffset += 4; - this.trackID = view.getUint32(headerOffset); - headerOffset += 4; + this.width = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; - // Reserved - // A 32-bit integer that is reserved for use by Apple. Set this field to 0. - headerOffset += 4; + this.height = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; - this.duration = view.getUint32(headerOffset); - headerOffset += 4; + return headerOffset; + }; +}; - // Reserved - // An 8-byte value that is reserved for use by Apple. Set this field to 0. - headerOffset += 8; +Atom.constructorMap['tkhd'] = TrackHeaderAtom.bind(null); - this.layer = view.getUint16(headerOffset); - headerOffset += 2; +class MediaHeaderAtom extends FullBox { + constructor(parent) { + super(parent); - this.alternateGroup = view.getUint16(headerOffset); - headerOffset += 2; + this.description = "Media Header Atom"; + this.creationTime = 0; + this.modificationTime = 0; + this.timeScale = 0; + this.duration = 0; + this.language = 0; + this.quality = 0; + }; - this.volume = view.getUint16(headerOffset) / (1 << 8); - headerOffset += 2; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - // Reserved - // A 16-bit integer that is reserved for use by Apple. Set this field to 0. - headerOffset += 2; + this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; - this.trackMatrix = new Array(3); - // a, b, u: - this.trackMatrix[0] = new Array(3); - this.trackMatrix[0][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[0][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[0][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - // c, d, v: - this.trackMatrix[1] = new Array(3); - this.trackMatrix[1][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[1][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[1][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - // x, y, w: - this.trackMatrix[2] = new Array(3); - this.trackMatrix[2][0] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[2][1] = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - this.trackMatrix[2][2] = view.getUint32(headerOffset) / (1 << 30); - headerOffset += 4; - - this.width = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; - - this.height = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; -} + this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; -var MediaHeaderAtom = function (buffer, offset) { - this.super(MediaHeaderAtom).constructor.call(this, buffer, offset); -} + this.timeScale = view.getUint32(headerOffset); + headerOffset += 4; -MediaHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); + this.duration = view.getUint32(headerOffset); + headerOffset += 4; -MediaHeaderAtom.prototype.setDefaults = function () { - this.super(MediaHeaderAtom).setDefaults.call(this); + this.language = view.getUint16(headerOffset); + headerOffset += 2; - this.description = "Media Header Atom"; - this.creationTime = 0; - this.modificationTime = 0; - this.timeScale = 0; - this.duration = 0; - this.language = 0; - this.quality = 0; -} + this.quality = view.getUint16(headerOffset); + headerOffset += 2; -MediaHeaderAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(MediaHeaderAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + return headerOffset; + }; +}; - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; +Atom.constructorMap['mdhd'] = MediaHeaderAtom.bind(null); - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; +class HandlerReferenceBox extends FullBox { + constructor(parent) { + super(parent); - this.timeScale = view.getUint32(headerOffset); - headerOffset += 4; + this.description = 'Handler Reference Box'; + this.handlerType = ''; + this.name = ''; + }; - this.duration = view.getUint32(headerOffset); - headerOffset += 4; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - this.language = view.getUint16(headerOffset); - headerOffset += 2; + // unsigned int(32) predefined = 0; + headerOffset += 4; - this.quality = view.getUint16(headerOffset); - headerOffset += 2; + var array = new Uint8Array(buffer, offset + headerOffset, 4); + this.handlerType = String.fromCharCode.apply(null, array); + headerOffset += 4; + + // unsigned int(32)[3] reserved = 0; + headerOffset += 12; + + var remaining = this.size - headerOffset; + array = new Uint8Array(buffer, offset + headerOffset, remaining); + this.name = String.fromCharCode.apply(null, array); + + headerOffset += remaining; + + return headerOffset; + }; }; -var SyncSampleAtom = function (buffer, offset) { - this.super(SyncSampleAtom).constructor.call(this, buffer, offset); +Atom.constructorMap['hdlr'] = HandlerReferenceBox.bind(null); + +class SyncSampleAtom extends Atom { + constructor(parent) { + super(parent); + + this.description = "Sync Sample Atom"; + this.version = 0; + this.flags = 0; + this.entries = 0; + this.syncSamples = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.version = view.getUint8(headerOffset); + headerOffset += 1; + + // 'flags' is a 3-byte field, so retrieve from one extra byte and concatenate + this.flags = (view.getUint8(headerOffset) << 8) + view.getUint16(headerOffset + 1); + headerOffset += 3; + + this.entries = view.getUint32(headerOffset); + headerOffset += 4; + + this.syncSamples = new Uint32Array(this.entries); + var i = 0; + while (headerOffset < this.size) { + var sampleNumber = view.getUint32(headerOffset); + headerOffset += 4; + this.syncSamples[i] = sampleNumber; + ++i; + } + + return headerOffset; + }; }; -SyncSampleAtom.prototype = Object.create(Atom.prototype); +Atom.constructorMap['stss'] = SyncSampleAtom.bind(null); -SyncSampleAtom.prototype.setDefaults = function () { - this.super(SyncSampleAtom).setDefaults.call(this); +class TimeToSampleAtom extends FullBox { + constructor(parent) { + super(parent); + this.description = "Time-to-Sample Atom"; + this.entries = 0; + + Object.defineProperty(this, "timeToSamples", { + value: null, + writable: true, + enumerable: false, + configurable: true, + }); + }; - this.description = "Sync Sample Atom"; - this.version = 0; - this.flags = 0; - this.entries = 0; - this.syncSamples = []; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.entries = view.getUint32(headerOffset); + headerOffset += 4; + + this.timeToSamples = new Array(this.entries); + var i = 0; + + while (headerOffset < this.size) { + var sampleCount = view.getUint32(headerOffset); + headerOffset += 4; + + var sampleDuration = view.getUint32(headerOffset); + headerOffset += 4; + + this.timeToSamples[i] = [sampleCount, sampleDuration]; + ++i; + } + + return headerOffset; + }; + + + timeForIndex(index) + { + var sampleSum = 0; + var timeSum = 0; + + for (var j = 0; j < this.timeToSamples.length; ++j) { + var samplesWithTime = this.timeToSamples[j][0]; + var sampleLength = this.timeToSamples[j][1]; + var samplesThisPass = Math.min(index - sampleSum, samplesWithTime); + if (isNaN(samplesWithTime) || isNaN(sampleLength)) + break; + + sampleSum += samplesThisPass; + timeSum += samplesThisPass * sampleLength; + + if (sampleSum >= index) + break; + } + + return timeSum; + }; }; -SyncSampleAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(SyncSampleAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); +Atom.constructorMap['stts'] = TimeToSampleAtom.bind(null); - this.version = view.getUint8(headerOffset); - headerOffset += 1; +class SampleSizeAtom extends FullBox { + constructor(parent) { + super(parent); + this.description = "Sample Size Atom"; + this.sampleSize = 0; + this.entries = 0; + + Object.defineProperty(this, "sampleSizes", { + value: null, + writable: true, + enumerable: false, + configurable: true, + }); + }; - // 'flags' is a 3-byte field, so retrieve from one extra byte and concatenate - this.flags = (view.getUint8(headerOffset) << 8) + view.getUint16(headerOffset + 1); - headerOffset += 3; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - this.entries = view.getUint32(headerOffset); - headerOffset += 4; + this.sampleSize = view.getUint32(headerOffset); + headerOffset += 4; - this.syncSamples = new Uint32Array(this.entries); - var i = 0; - while (headerOffset < this.size) { - var sampleNumber = view.getUint32(headerOffset); + this.entries = view.getUint32(headerOffset); headerOffset += 4; - this.syncSamples[i] = sampleNumber; - ++i; - } + + this.sampleSizes = new Uint32Array(this.entries); + var i = 0; + + while (headerOffset < this.size) { + this.sampleSizes[i] = view.getUint32(headerOffset); + headerOffset += 4; + ++i; + } + + return headerOffset; + }; }; -var TimeToSampleAtom = function (buffer, offset) { - this.super(TimeToSampleAtom).constructor.call(this, buffer, offset); +Atom.constructorMap['stsz'] = SampleSizeAtom.bind(null); + +class SampleDescriptionBox extends FullBox { + constructor(parent) { + super(parent); + + this.description = "Sample Description Box"; + this.childAtoms = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + if (this.parent.type !== 'stbl' || this.parent.parent.type !== 'minf' || this.parent.parent.parent.type !== 'mdia') + return; + + var handlerBox = this.parent.parent.parent.getAtomByType('hdlr'); + if (!handlerBox) + return; + + var entryCount = view.getUint32(headerOffset); + headerOffset += 4; + + for (var index = 0; index < entryCount; ++index) { + var entry; + var type = Atom.getType(buffer, offset + headerOffset); + if (typeof(Atom.constructorMap[type]) !== 'undefined') + entry = Atom.create(buffer, offset + headerOffset); + else { + switch (handlerBox.handlerType) { + case 'soun': + entry = new AudioSampleEntry(this); + break; + case 'vide': + entry = new VisualSampleEntry(this); + break; + case 'hint': + entry = new HintSampleDescriptionBox(this); + break; + case 'meta': + entry = new MetadataSampleDescriptionBox(this); + break; + default: + return; + } + entry.parse(buffer, offset + headerOffset); + } + headerOffset += entry.size; + this.childAtoms.push(entry); + } + + return headerOffset; + }; }; -TimeToSampleAtom.prototype = Object.create(VersionFlagsAtom.prototype); +Atom.constructorMap['stsd'] = SampleDescriptionBox.bind(null); -TimeToSampleAtom.prototype.setDefaults = function () { - this.super(TimeToSampleAtom).setDefaults.call(this); +class SampleEntry extends Atom { + constructor(parent) { + super(parent); + + this.description = 'Sample Entry'; + this.dataReferenceIndex = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + // unsigned int(8)[6] reserved = 0 + headerOffset += 6; + + this.dataReferenceIndex = view.getUint16(headerOffset); + headerOffset += 2; - this.description = "Time-to-Sample Atom"; - this.entries = 0; - - Object.defineProperty(this, "timeToSamples", { - value: null, - writable: true, - enumerable: false, - configurable: true, - }); + return headerOffset; } +}; + +class AudioSampleEntry extends SampleEntry { + constructor(parent) { + super(parent); + this.description = 'Audio Sample Entry'; + this.channelCount = 0; + this.sampleSize = 0; + this.sampleRate = 0; + }; -TimeToSampleAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(TimeToSampleAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - this.entries = view.getUint32(headerOffset); - headerOffset += 4; + // unsigned int(32)[2] reserved = 0 + headerOffset += 8; - this.timeToSamples = new Array(this.entries); - var i = 0; + this.channelCount = view.getUint16(headerOffset); + headerOffset += 2; - while (headerOffset < this.size) { - var sampleCount = view.getUint32(headerOffset); + this.sampleSize = view.getUint16(headerOffset); + headerOffset += 2; + + // unsigned int(16) pre_defined = 0 + // const unsigned int(16) reserved = 0 headerOffset += 4; - var sampleDuration = view.getUint32(headerOffset); + this.sampleRate = (view.getUint32(headerOffset) >> 16) & 0xFFFF; headerOffset += 4; - this.timeToSamples[i] = [sampleCount, sampleDuration]; - ++i; + return headerOffset; + }; +}; + +class MP4AudioSampleEntry extends AudioSampleEntry { + constructor(parent) { + super(parent); + this.description = 'MP4 Audio Sample Entry'; + this.childAtoms = []; + }; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var ES = new ESDBox(this); + ES.parse(buffer, offset+ headerOffset); + this.childAtoms.push(ES); + headerOffset += ES.size; + + return headerOffset; + }; +}; +Atom.constructorMap['mp4a'] = MP4AudioSampleEntry.bind(null); + +class ESDBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Sample Description Box' } -} -var SampleSizeAtom = function (buffer, offset) { - this.super(SampleSizeAtom).constructor.call(this, buffer, offset); -} + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); -SampleSizeAtom.prototype = Object.create(VersionFlagsAtom.prototype); - -SampleSizeAtom.prototype.setDefaults = function () { - this.super(SampleSizeAtom).setDefaults.call(this); - - this.description = "Sample Size Atom"; - this.sampleSize = 0; - this.entries = 0; - - Object.defineProperty(this, "sampleSizes", { - value: null, - writable: true, - enumerable: false, - configurable: true, - }); -} + this.descriptor = new ESDescriptor(this); + headerOffset += this.descriptor.parse(buffer, offset + headerOffset); + + return headerOffset; + } +}; + +Atom.constructorMap['esds'] = ESDBox.bind(null); + +class BaseDescriptor { + constructor(parent) { + Object.defineProperty(this, "parent", { + value: parent, + writable: true, + enumerable: false, + configurable: true, + }); + Object.defineProperty(this, "description", { + value: "Abstract Descriptor", + writable: true, + enumerable: false, + configurable: true, + }); + + this.tag = 0; + this.size = 0; + }; + parse(buffer, offset) { + var headerOffset = 0; + var view = new DataView(buffer, offset); + + this.tag = view.getUint8(headerOffset); + headerOffset += 1; + + var tagInfo = BaseDescriptor.TagMap[this.tag]; + if (typeof(tagInfo) !== 'undefined') + this.name = tagInfo.name; + + // BaseDescriptor starts at a size of 2, and can be extended: + this.size = 2; + for (var i = 0; i < 4; ++i) { + var nextSizeByte = view.getUint8(headerOffset); + headerOffset += 1; + + var msb = nextSizeByte & 0x80; + var size = nextSizeByte & 0x7f; + this.size += size; + + if (!msb) + break; + } + return headerOffset; + }; +}; + +BaseDescriptor.TagMap = { + 3: { name: 'ES_DescrTag' }, + 4: { name: 'DecoderConfigDescrTag' }, + 5: { name: 'DecSpecificInfoTag' }, +}; + +class ESDescriptor extends BaseDescriptor { + constructor(parent) { + super(parent); + this.description = "ES Descriptor" + this.ES_ID = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset);; + var view = new DataView(buffer, offset); + + this.ES_ID = view.getUint16(headerOffset); + headerOffset += 2; + + var nextByte = view.getUint8(headerOffset); + headerOffset += 1; -SampleSizeAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(SampleSizeAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + this.streamDependencyFlag = nextByte & (1 << 7); + this.urlFlag = nextByte & (1 << 6); + this.ocrStreamFlag = nextByte & (1 << 5); + this.streamPriority = nextByte & 0x1f; - this.sampleSize = view.getUint32(headerOffset); - headerOffset += 4; + if (this.streamDependencyFlag) { + this.dependsOn_ES_Number = view.getUint16(headerOffset); + headerOffset += 2; + } - this.entries = view.getUint32(headerOffset); - headerOffset += 4; + if (this.urlFlag) { + var urlLength = view.getUint8(headerOffset); + headerOffset += 1; - this.sampleSizes = new Uint32Array(this.entries); - var i = 0; + var array = new Uint8Array(buffer, offset + headerOffset, urlLength); + headerOffset += urlLength; + this.url = String.fromCharCode.apply(null, array); + } - while (headerOffset < this.size) { - this.sampleSizes[i] = view.getUint32(headerOffset); + if (this.ocrStreamFlag) { + this.ocr_ES_ID = view.getUint16(headerOffset); + headerOffset += 2; + } + + this.decoderConfigDescriptor = new DecoderConfigDescriptor(this); + headerOffset += this.decoderConfigDescriptor.parse(buffer, offset + headerOffset); + + return headerOffset; + } +}; + +class DecoderConfigDescriptor extends BaseDescriptor { + constructor(parent) { + super(parent); + this.description = "Decoder Config Descriptor" + this.streamType = 0; + this.objectTypeIndication = 0; + this.upStream = 0; + this.specificInfoFlag = 0; + this.bufferSizeDB = 0; + this.maxBitrate = 0; + this.avgBitrate = 0; + this.specificInfo = []; + }; + + parse(buffer, offset) + { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.objectTypeIndication = view.getUint8(headerOffset); + headerOffset += 1; + + var nextByte = view.getUint8(headerOffset); + this.streamType = (nextByte >> 2) & 0x3f; + this.upStream = nextByte & 0x2; + this.specificInfoFlag = nextByte & 0x1; + headerOffset += 1; + + var next4Bytes = view.getUint32(headerOffset); + this.bufferSizeDB = (next4Bytes >> 8) & 0xFFFFFF + headerOffset += 3; + + this.maxBitrate = view.getUint32(headerOffset); + headerOffset += 4; + + this.avgBitrate = view.getUint32(headerOffset); headerOffset += 4; - ++i; + + while (this.specificInfoFlag && headerOffset < this.size) { + var specificInfo = new DecoderSpecificInfo(this); + specificInfo.parse(buffer, offset + headerOffset) + headerOffset += specificInfo.size; + + this.specificInfo.push(specificInfo); + } + + return headerOffset; } -} +}; -var TrackExtendsAtom = function (buffer, offset) { - this.super(TrackExtendsAtom).constructor.call(this, buffer, offset); +class DecoderSpecificInfo extends BaseDescriptor { + constructor(parent) { + // 'Audio ISO/IEC 14496-3' && 'AudioStreamType' + if (parent.objectTypeIndication == 0x40 && parent.streamType == 0x5) + return new AudioSpecificConfig(parent); + + super(parent); + this.description = 'Decoder Specific Info'; + } } -TrackExtendsAtom.prototype = Object.create(VersionFlagsAtom.prototype); +class AudioSpecificConfig extends BaseDescriptor { + constructor(parent) { + super(parent); + this.audioObjectType = 0; + this.samplingFrequencyIndex = 0; + this.channelConfiguration = 0; + } + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + if (this.size < headerOffset) + return; + + var array = new Uint8Array(buffer, offset + headerOffset, this.size - headerOffset); + var bitReader = new BitReader(array, 0); + + this.audioObjectType = bitReader.readBits(5); + if (this.audioObjectType === 0x1f) + this.audioObjectType = 32 + bitReader.readBits(6); -TrackExtendsAtom.prototype.setDefaults = function () { - this.super(TrackExtendsAtom).setDefaults.call(this); + this.samplingFrequencyIndex = bitReader.readBits(4); + if (this.samplingFrequencyIndex === 0xf) + this.samplingFrequencyIndex += bitReader.readBits(24); - this.description = "Track Extends Atom"; - this.trackID = 0; - this.default_sample_description_index = 0; - this.default_sample_duration = 0; - this.default_sample_size = 0; - this.default_sample_flags = 0; + return headerOffset; + } } -TrackExtendsAtom.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(TrackExtendsAtom).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); +class VisualSampleEntry extends SampleEntry { + constructor(parent) { + super(parent); + + this.description = 'Visual Sample Entry'; + this.width = 0; + this.height = 0; + this.horizontalResolution = 0; + this.verticalResolution = 0; + this.frameCount; + this.compressorName; + this.depth; + this.childAtoms = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + // unsigned int(16) pre_defined = 0 + // const unsigned int(16) reserved = 0 + // unsigned int(32)[3] pre_defined = 0 + headerOffset += 16; + + this.width = view.getUint16(headerOffset); + headerOffset += 2; + + this.height = view.getUint16(headerOffset); + headerOffset += 2; + + this.horizontalResolution = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; - this.trackID = view.getUint32(headerOffset); - headerOffset += 4; + this.verticalResolution = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; - this.default_sample_description_index = view.getUint32(headerOffset); - headerOffset += 4; + // const unsigned int(32) reserved = 0 + headerOffset += 4; - this.default_sample_duration = view.getUint32(headerOffset); - headerOffset += 4; + this.frameCount = view.getUint16(headerOffset); + headerOffset += 2; - this.default_sample_size = view.getUint32(headerOffset); - headerOffset += 4; + var array = new Uint8Array(buffer, offset + headerOffset, 32); + this.compressorName = String.fromCharCode.apply(null, array); + headerOffset += 32; - this.default_sample_flags = view.getUint32(headerOffset); - headerOffset += 4; -} + this.depth = view.getUint16(headerOffset); + headerOffset += 2; -var OriginalFormatBox = function (buffer, offset) { - this.super(OriginalFormatBox).constructor.call(this, buffer, offset); -} + // int(16) pre_defined = -1; + headerOffset += 2; -OriginalFormatBox.prototype = Object.create(Atom.prototype); + while (this.size - headerOffset > 8) { + var childAtom = Atom.create(buffer, offset + headerOffset, this); + if (!childAtom) + break; + headerOffset += childAtom.size; + this.childAtoms.push(childAtom); + } + return headerOffset; + }; +}; -OriginalFormatBox.prototype.setDefaults = function () { - this.super(OriginalFormatBox).setDefaults.call(this); +class AVCConfigurationBox extends Atom { + constructor(parent) { + super(parent); - this.description = "Original Format Box"; - this.dataFormat = 0; -} + this.description = 'AVC Configuration Box'; + this.configurationVersion = 0; + this.AVCProfileIndication = 0; + this.profileCompatibility = 0; + this.AVCLevelIndication = 0; + this.sequenceParameterSets = []; + this.pictureParameterSets = []; + }; -OriginalFormatBox.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(OriginalFormatBox).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); + parse(buffer, offset) + { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - this.dataFormat = view.getUint32(headerOffset); - headerOffset += 4; -} + this.configurationVersion = view.getUint8(headerOffset); + ++headerOffset; -var SchemeTypeBox = function (buffer, offset) { - this.super(SchemeTypeBox).constructor.call(this, buffer, offset); -} + this.AVCProfileIndication = view.getUint8(headerOffset); + ++headerOffset; -SchemeTypeBox.prototype = Object.create(VersionFlagsAtom.prototype); + this.profileCompatibility = view.getUint8(headerOffset); + ++headerOffset; -SchemeTypeBox.prototype.setDefaults = function () { - this.super(SchemeTypeBox).setDefaults.call(this); + this.AVCLevelIndication = view.getUint8(headerOffset); + ++headerOffset; - this.description = "Scheme Type Box"; - this.schemeType = 0; - this.schemeVersion = 0; - this.schemeURL = 0; -} + var lengthSizeMinusOne = view.getUint8(headerOffset) & 0x3; + ++headerOffset; + + var numOfSequenceParameterSets = view.getUint8(headerOffset) & 0x1f; + ++headerOffset; + + for (var index = 0; index < numOfSequenceParameterSets; ++index) { + var sequenceParameterSetLength = view.getUint16(headerOffset); + headerOffset += 2; + + this.sequenceParameterSets.push(new Uint8Array(buffer, offset + headerOffset, sequenceParameterSetLength)); + headerOffset += sequenceParameterSetLength; + } + + var numOfPictureParameterSets = view.getUint8(headerOffset) & 0x1f; + ++headerOffset; + + for (index = 0; index < numOfPictureParameterSets; ++index) { + var pictureParameterSetLength = view.getUint16(headerOffset); + headerOffset += 2; + + this.pictureParameterSets.push(new Uint8Array(buffer, offset + headerOffset, pictureParameterSetLength)); + headerOffset += pictureParameterSetLength; + } + + if ([100, 110, 122, 144].indexOf(this.AVCProfileIndication) >= 0) { + + // bit(6) reserved = '111111'b + this.chromaFormat = view.getUint8(headerOffset) & 0x3; + ++headerOffset; + + // bit(6) reserved = '111111'b + this.bitDepthLumaMinus8 = view.getUint8(headerOffset) & 0x3; + ++headerOffset; + + // bit(5) reserved = '11111'b;
 + this.bitDepthChromaMinus8 = view.getUint8(headerOffset) & 0x7; + ++headerOffset; + + if (headerOffset >= this.size) + return headerOffset; + + var numOfSequenceParameterSetExt = view.getUint8(headerOffset); + this.sequenceParameterSets = []; + ++headerOffset; + + for (index = 0; index < numOfSequenceParameterSetExt; ++index) { + var sequenceParameterSetLength = view.getUint16(headerOffset); + headerOffset += 2; + + this.sequenceParameterSets.push(new Uint8Array(buffer, offset + headerOffset, sequenceParameterSetLength)); + headerOffset += sequenceParameterSetLength; + } + } + + return headerOffset; + }; +}; + +Atom.constructorMap['avcC'] = AVCConfigurationBox.bind(null); + +class HEVCConfigurationBox extends Atom { + constructor(parent) { + super(parent); + + this.description = 'HEVC Configuration Box'; + this.configuration_version = 0; + this.general_profile_space = 0; + this.general_tier_flag = 0; + this.general_profile_idc = 0; + this.general_profile_compatibility_flags = 0; + this.general_constraint_indicator_flags = 0; + this.general_level_idc = 0; + this.min_spatial_segmentation_idc = 0; + this.parallelismType = 0; + this.chromaFormat = 0; + this.bitDepthLumaMinus8 = 0; + this.bitDepthChromaMinus8 = 0; + this.avgFrameRate = 0; + this.constantFrameRate = 0; + this.numTemporalLayers = 0; + this.temporalIdNested = 0; + }; + + parse(buffer, offset) + { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.configuration_version = view.getUint8(headerOffset); + ++headerOffset; + + var byte = view.getUint8(headerOffset); + ++headerOffset; + + this.general_profile_space = (byte & 0x00C0) >> 6; + this.general_tier_flag = (byte & 0x0020) >> 5; + this.general_profile_idc = byte & 0x001F; + + this.general_profile_compatibility_flags = view.getUint32(headerOffset); + headerOffset += 4; + + this.general_constraint_indicator_flags = view.getUint16(headerOffset) << 32 + + view.getUint32(headerOffset + 2); + headerOffset += 6; + + this.general_level_idc = view.getUint8(headerOffset); + ++headerOffset; + + // bit(4) reserved = ‘1111’b; + this.min_spatial_segmentation_idc = view.getUint16(headerOffset) & 0x0FFF; + headerOffset += 2; + + // bit(6) reserved = ‘111111’b; + this.parallelismType = view.getUint8(headerOffset) & 0x03; + ++headerOffset; + + // bit(6) reserved = ‘111111’b; + this.chromaFormat = view.getUint8(headerOffset) & 0x03; + ++headerOffset; + + // bit(5) reserved = ‘11111’b; + this.bitDepthLumaMinus8 = view.getUint8(headerOffset) & 0x07; + ++headerOffset; + + // bit(5) reserved = ‘11111’b; + this.bitDepthChromaMinus8 = view.getUint8(headerOffset) & 0x07; + ++headerOffset; + + this.avgFrameRate = view.getUint16(headerOffset); + headerOffset += 2; + + byte = view.getUint8(headerOffset); + ++headerOffset; + + this.constantFrameRate = (byte & 0xC0) >> 6; + this.numTemporalLayers = (byte & 0x38) >> 3; + this.temporalIdNested = (byte & 0x04) >> 2; + this.lengthSizeMinusOne = byte & 0x02; + + return this.size; + }; +}; + +Atom.constructorMap['hvcC'] = HEVCConfigurationBox.bind(null); + +class CleanApertureBox extends Atom { + constructor(parent) { + super(parent); + + this.description = 'Clean Aperture Box'; + this.cleanApertureWidthN = 0; + this.cleanApertureWidthD = 0; + this.cleanApertureHeightN = 0; + this.cleanApertureHeightD = 0; + this.horizOffN = 0; + this.horizOffD = 0; + this.vertOffN = 0; + this.vertOffD = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.cleanApertureWidthN = view.getUint32(headerOffset); + headerOffset += 4; + + this.cleanApertureWidthD = view.getUint32(headerOffset); + headerOffset += 4; + + this.cleanApertureHeightN = view.getUint32(headerOffset); + headerOffset += 4; + + this.cleanApertureHeightD = view.getUint32(headerOffset); + headerOffset += 4; + + this.horizOffN = view.getUint32(headerOffset); + headerOffset += 4; + + this.horizOffD = view.getUint32(headerOffset); + headerOffset += 4; + + this.vertOffN = view.getUint32(headerOffset); + headerOffset += 4; + + this.vertOffD = view.getUint32(headerOffset); + headerOffset += 4; -SchemeTypeBox.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(SchemeTypeBox).parse.call(this, buffer, offset); - var view = new DataView(buffer, offset); - - this.schemeType = view.getUint32(headerOffset); - headerOffset += 4; - this.schemeVersion = view.getUint32(headerOffset); - headerOffset += 4; - if (this.flags & 0x1) { - var array = new Uint8Array(buffer, headerOffset, this.size - headerOffset); - this.schemeURL = String.fromCharCode.apply(null, array); + return headerOffset; + }; +}; + +Atom.constructorMap['clap'] = CleanApertureBox.bind(null); + +class TrackExtendsAtom extends FullBox { + constructor(parent) { + super(parent); + this.description = "Track Extends Atom"; + this.trackID = 0; + this.default_sample_description_index = 0; + this.default_sample_duration = 0; + this.default_sample_size = 0; + this.default_sample_flags = 0; } -} -var SchemeInfoBox = function (buffer, offset) { - this.super(SchemeInfoBox).constructor.call(this, buffer, offset); -} + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.trackID = view.getUint32(headerOffset); + headerOffset += 4; -SchemeInfoBox.prototype = Object.create(VersionFlagsAtom.prototype); + this.default_sample_description_index = view.getUint32(headerOffset); + headerOffset += 4; -SchemeInfoBox.prototype.setDefaults = function () { - this.super(SchemeInfoBox).setDefaults.call(this); + this.default_sample_duration = view.getUint32(headerOffset); + headerOffset += 4; - this.description = "Scheme Information Box"; - this.schemeSpecificData = 0; -} + this.default_sample_size = view.getUint32(headerOffset); + headerOffset += 4; + + this.default_sample_flags = view.getUint32(headerOffset); + headerOffset += 4; + + return headerOffset; + }; +}; + +Atom.constructorMap['trex'] = TrackExtendsAtom.bind(null); + +class OriginalFormatBox extends Atom { + constructor(parent) { + super(parent); + this.description = "Original Format Box"; + this.dataFormat = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + + var array = new Uint8Array(buffer, offset + headerOffset, 4); + this.dataFormat = String.fromCharCode.apply(null, array); + headerOffset += 4; + + return headerOffset; + }; +}; + +Atom.constructorMap['frma'] = OriginalFormatBox.bind(null); + +class SchemeTypeBox extends FullBox { + constructor(parent) { + super(parent); + this.description = "Scheme Type Box"; + this.schemeType = 0; + this.schemeVersion = 0; + this.schemeURL = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + var array = new Uint8Array(buffer, offset + headerOffset, 4); + this.schemeType = String.fromCharCode.apply(null, array); + headerOffset += 4; + + this.schemeVersion = view.getUint32(headerOffset); + headerOffset += 4; + + if (this.flags & 0x1) { + var remaining = this.size - headerOffset; + array = new Uint8Array(buffer, offset + headerOffset, remaining); + headerOffset += remaining; + this.schemeURL = String.fromCharCode.apply(null, array); + } + + return headerOffset; + }; +}; + +Atom.constructorMap['schm'] = SchemeTypeBox.bind(null); + +class TrackEncryptionBox extends FullBox { + constructor(parent) { + super(parent); + + this.description = "Track Encryption Box"; + this.defaultCryptByteBlock = 0; + this.defaultSkipByteBlock = 0; + this.defaultIsProtected = 0; + this.defaultPerSampleIVSize = 0; + this.defaultKID = ''; + this.defaultConstantIV = null; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + // unsigned int(8) reserved = 0 + ++headerOffset; + + if (!this.version) { + // unsigned int(8) reserved = 0 + ++headerOffset + } else { + this.defaultCryptByteBlock = (view.getUint8(headerOffset) >> 4) & 0xF; + this.defaultSkipByteBlock = view.getUint8(headerOffset) & 0xF; + ++headerOffset; + } + + this.defaultIsProtected = view.getUint8(headerOffset); + ++headerOffset; + + this.defaultPerSampleIVSize = view.getUint8(headerOffset); + ++headerOffset; + + var KIDArrayView = new Uint8Array(buffer, offset + headerOffset, 16); + this.defaultKID = String.prototype.concat.apply("0x", Array.prototype.map.call(KIDArrayView, function(value){ return value.toString(16); })); + headerOffset += 16; + + if (this.defaultIsProtected && !this.defaultPerSampleIVSize) { + var size = view.getUint8(headerOffset); + ++headerOffset; + + this.defaultConstantIV = new Uint8Array(buffer, offset + headerOffset, size); + headerOffset += size; + } + + return headerOffset; + }; +}; + +Atom.constructorMap['tenc'] = TrackEncryptionBox.bind(null); + +class SegmentIndexBox extends FullBox { + constructor(parent) { + super(parent); + + this.description = "Segment Index Box"; + this.referenceID = 0; + this.timeScale = 0; + this.earliestPresentationTime = 0; + this.firstOffset = 0; + this.references = []; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.referenceID = view.getUint32(headerOffset); + headerOffset += 4; + + this.timeScale = view.getUint32(headerOffset); + headerOffset += 4; + + if (this.version == 1) { + var upper = view.getUint32(headerOffset); + headerOffset += 4; + var lower = view.getUint32(headerOffset); + headerOffset += 4; + + this.earliestPresentationTime = (upper << 32) + lower; -SchemeInfoBox.prototype.parse = function (buffer, offset) { - var headerOffset = this.super(SchemeInfoBox).parse.call(this, buffer, offset); - this.schemeSpecificData = buffer.slice(headerOffset, this.size - headerOffset); + upper = view.getUint32(headerOffset); + headerOffset += 4; + lower = view.getUint32(headerOffset); + headerOffset += 4; + + this.firstOffset = (upper << 32) + lower; + } else { + this.earliestPresentationTime = view.getUint32(headerOffset); + headerOffset += 4; + + this.firstOffset = view.getUint32(headerOffset); + headerOffset += 4; + } + + headerOffset += 2; // Reserved uint(16) + + this.referenceCount = view.getUint16(headerOffset); + headerOffset += 2; + + this.references = []; + + for (var i = 0; i < this.referenceCount; ++i) { + var value = view.getUint32(headerOffset); + headerOffset += 4; + + var reference = {}; + reference.type = (value & 0x80000000) == 0x80000000; + reference.size = value & ~0x80000000; + + reference.subsegmentDuration = view.getUint32(headerOffset); + headerOffset += 4; + + value = view.getUint32(headerOffset); + headerOffset += 4; + + reference.startsWithSAP = (value & 0x80000000) == 0x80000000; + reference.SAPType = (value & 0x70000000) >> 28; + reference.SAPDeltaTime = value & ~0xF0000000; + this.references.push(reference); + } + + this.totalDuration = this.references.reduce(function(previousValue, reference) { + return previousValue + reference.subsegmentDuration; + }, 0); + + return headerOffset; + }; +}; + +Atom.constructorMap['sidx'] = SegmentIndexBox.bind(null); + +class ProtectionSystemBox extends FullBox { + constructor(parent) { + super(parent) + this.description = "Protection System Box"; + this.systemID = 0; + this.KIDs = []; + this.data = null; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + var UUIDArrayView = new Uint8Array(buffer, offset + headerOffset, 16); + this.systemID = String.prototype.concat.apply("0x", Array.prototype.map.call(UUIDArrayView, function(value){ + return value.toString(16); + })); + headerOffset += 16; + + if (this.version > 0) { + var kidCount = view.getUint32(headerOffset); + headerOffset += 4; + + for (var index = 0; index < kidCount; ++index) { + var KIDArrayView = new Uint8Array(buffer, offset + headerOffset, 16); + var KIDString = String.prototype.concat.apply("0x", Array.prototype.map.call(KIDArrayView, function(value){ return value.toString(16); })); + this.KIDs.push(KIDString); + headerOffset += 16; + } + } + + var dataSize = view.getUint32(headerOffset); + this.data = new Uint8Array(buffer, offset + headerOffset, dataSize); + headerOffset += dataSize; + + return headerOffset; + }; +}; + +Atom.constructorMap['pssh'] = ProtectionSystemBox.bind(null); + +class MovieExtendsHeaderBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Movie Extends Header Box'; + this.duration = 0; + } + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.duration = view.getUint32(headerOffset); + } } +Atom.constructorMap['mehd'] = MovieExtendsHeaderBox.bind(null); + + +class MovieFragmentHeaderBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Movie Fragment Header Box'; + this.sequenceNumber = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.sequenceNumber = view.getUint32(headerOffset); + headerOffset += 4; + }; +}; + +Atom.constructorMap['mfhd'] = MovieFragmentHeaderBox.bind(null); + +class TrackFragmentHeaderBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Track Fragment Header Box'; + this.baseDataOffsetPresent = false; + this.sampleDescriptionIndexPresent = false; + this.defaultSampleDurationPresent = false; + this.defaultSampleSizePresent = false; + this.defaultSampleFlagsPresent = false; + this.durationIsEmpty = false; + this.defaultBaseIsMoof = false; + this.trackID = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + + this.baseDataOffsetPresent = this.flags & 0x00001 ? true : false; + this.sampleDescriptionIndexPresent = this.flags & 0x00002 ? true : false; + this.defaultSampleDurationPresent = this.flags & 0x00008 ? true : false; + this.defaultSampleSizePresent = this.flags & 0x00010 ? true : false; + this.defaultSampleFlagsPresent = this.flags & 0x00020 ? true : false; + this.durationIsEmpty = this.flags & 0x10000 ? true : false; + this.defaultBaseIsMoof = this.flags & 0x20000 ? true : false; + + var view = new DataView(buffer, offset); + + this.trackID = view.getUint32(headerOffset); + headerOffset += 4; + + if (this.baseDataOffsetPresent) { + var upper = view.getUint32(headerOffset); + var lower = view.getUint32(headerOffset + 4); + headerOffset += 8; + + this.baseDataOffset = (upper << 32) + lower; + } + + if (this.sampleDescriptionIndexPresent) { + this.sampleDescriptionIndex = view.getUint32(headerOffset); + headerOffset += 4; + } + + if (this.defaultSampleDurationPresent) { + this.defaultSampleDuration = view.getUint32(headerOffset); + headerOffset += 4; + } + + if (this.defaultSampleSizePresent) { + this.defaultSampleSize = view.getUint32(headerOffset); + headerOffset += 4; + } + + if (this.defaultSampleFlagsPresent) { + this.defaultSampleFlags = view.getUint32(headerOffset); + headerOffset += 4; + } + + return headerOffset; + }; +}; + +Atom.constructorMap['tfhd'] = TrackFragmentHeaderBox.bind(null); + +class TrackFragmentRunBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Track Fragment Run Box'; + this.dataOffsetPresent = false; + this.firstSampleFlagsPresent = false; + this.sampleDurationPresent = false; + this.sampleSizePresent = false; + this.sampleFlagsPresent = false; + this.sampleCompositionTimeOffsetsPresent = false; + this.dataOffset; + this.samples = []; + this.duration = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + this.dataOffsetPresent = this.flags & 0x00001 ? true : false; + this.firstSampleFlagsPresent = this.flags & 0x00004 ? true : false; + this.sampleDurationPresent = this.flags & 0x00100 ? true : false; + this.sampleSizePresent = this.flags & 0x00200 ? true : false; + this.sampleFlagsPresent = this.flags & 0x00400 ? true : false; + this.sampleCompositionTimeOffsetsPresent = this.flags & 0x00800 ? true : false; + + var view = new DataView(buffer, offset); + + var sampleCount = view.getUint32(headerOffset); + headerOffset += 4; + + if (this.dataOffsetPresent) { + this.dataOffset = view.getUint32(headerOffset); + headerOffset += 4; + } + + if (this.firstSampleFlagsPresent) { + this.firstSampleFlags = view.getUint32(headerOffset); + headerOffset += 4; + } + + for (var index = 0; index < sampleCount; ++index) { + var sample = {} + if (this.sampleDurationPresent) { + sample.sampleDuration = view.getUint32(headerOffset); + this.duration += sample.sampleDuration; + headerOffset += 4; + } + + if (this.sampleSizePresent) { + sample.sampleSize = view.getUint32(headerOffset); + headerOffset += 4; + } + + if (this.sampleFlagsPresent) { + var sampleFlags = view.getUint32(headerOffset); + this.sampleFlags = { + isLeading: (sampleFlags & 0x0030) >> 4, + sampleDependsOn: (sampleFlags & 0x00C0) >> 6, + sampleIsDependedOn: (sampleFlags & 0x0300) >> 8, + sampleHasRedundency: (sampleFlags & 0x0C00) >> 10, + samplePaddingValue: (sampleFlags & 0x7000) >> 12, + sampleIsNonSyncSample: (sampleFlags & 0x8000) >> 15, + sampleDegredationPriority: (sampleFlags & 0xFFFF0000) >> 16, + } + headerOffset += 4; + } + + if (this.sampleCompositionTimeOffsetsPresent) { + sample.sampleCompositionTimeOffsets = !this.version ? view.getUint32(headerOffset) : view.getInt32(headerOffset); + headerOffset += 4; + } + this.samples.push(sample); + } + + return headerOffset; + }; +}; + +Atom.constructorMap['trun'] = TrackFragmentRunBox.bind(null); + +class TrackFragmentBaseMediaDecodeTimeBox extends FullBox { + constructor(parent) { + super(parent); + this.description = "Track Fragment Decode Time"; + this.baseMediaDecodeTime = 0; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + if (this.version === 1) { + var upper = view.getUint32(headerOffset); + var lower = view.getUint32(headerOffset + 4); + var sign = 1; + if (upper & (1 << 32)) { + sign = -1 + upper = ~upper; + lower = ~lower + 1; + } + + this.baseMediaDecodeTime = sign * ((upper << 32) + lower); + headerOffset += 8; + } else { + this.baseMediaDecodeTime = view.getUint32(headerOffset); + headerOffset += 4; + } + + return headerOffset; + }; +}; +Atom.constructorMap['tfdt'] = TrackFragmentBaseMediaDecodeTimeBox.bind(null); diff --git a/AtomTester.html b/AtomTester.html index 78587ca..5d1d4de 100644 --- a/AtomTester.html +++ b/AtomTester.html @@ -15,11 +15,13 @@ function checkForAtom(offset) { reader.onload = function(e) { - var atom = new Atom(e.target.result); - if (atom) + var atom = new Atom(); + if (atom) { + atom.parse(e.target.result, 0); readAtom(offset, atom.size); + } }; - var subset = file.slice(offset, offset + 16); + var subset = file.slice(offset, offset + 36); reader.readAsArrayBuffer(subset); } @@ -39,7 +41,6 @@ checkForAtom(0); } - function toDOMRepresentation(object) { if (object instanceof Atom) @@ -59,7 +60,13 @@ return document.createTextNode('"' + object + '"'); else if (object instanceof Date) return document.createTextNode(object.toLocaleString()); - else + else if (object instanceof Uint8Array) { + return Uint8ArrayNode(object); + } else if (object instanceof Object) + return toDOMNode(object); + else if (object === null) + return document.createTextNode('[null]'); + else return document.createTextNode(object.toString()); } @@ -89,6 +96,29 @@ } return output; } + + function Uint8ArrayNode(array) + { + var table = document.createElement('table'); + var width = 16; + for (var offset = 0; offset <= array.length; offset += width) { + var tr = document.createElement('tr'); + table.appendChild(tr); + + var th = document.createElement('th'); + tr.appendChild(th); + th.innerText = ('0000000' + offset.toString(16)).substr(-8) + ':'; + + for (var column = 0; column < width && offset + column < array.length; column += 2) { + var td = document.createElement('td'); + tr.appendChild(td); + td.innerText = ('00' + array[offset + column].toString(16)).substr(-2); + if (offset + column + 1 < array.length) + td.innerText += ('00' + array[offset + column + 1].toString(16)).substr(-2); + } + } + return table; + } function setup() { document.getElementById('file').addEventListener('change', onFileSelect, false); @@ -102,8 +132,9 @@ dd dl { margin: 0; vertical-align: top } dd span { display: inline-block; vertical-align: top; } - dt, dd { display: inline-block; min-width: 8em; } + dt, dd { display: inline-block; min-width: 14em; } dd { margin-left: 1em; } + dd th { font-weight: normal; text-align: right; } .description { text-align: center; font-weight: bold; } /* make the output display as a table */