X-Git-Url: http://id.pley.net/movie_parser.git/blobdiff_plain/578248d58df5e9cb5c8de2ea7cabff4560db9fa6..refs/heads/master:/Atom.js diff --git a/Atom.js b/Atom.js old mode 100644 new mode 100755 index 52e9e59..0875055 --- a/Atom.js +++ b/Atom.js @@ -1,547 +1,1866 @@ -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); - - switch (type) { - case 'ftyp': - return new FileTypeAtom(buffer, offset); - case 'moov': - case 'trak': - case 'mdia': - case 'minf': - 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); - 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, }); - this.size = 0; - this.type = ''; -} + Object.defineProperty(this, "description", { + value: "Undifferentiated Atom", + writable: true, + enumerable: false, + configurable: true, + }); + + this.offset = 0; + this.size = 0; + this.type = ''; + this.parent = parent; -Atom.prototype.parse = function(buffer, offset) { - // 'offset' is optional. - if (typeof(offset) == 'undefined') - offset = 0; + return this; + }; - if (buffer.byteLength - offset < this.minimumSize) - return 0; + static create(buffer, offset, parent) { + // 'offset' is optional. + if (arguments.length < 2) { + offset = 0; + } - var view = new DataView(buffer, offset, 4); - headerOffset = 0; + var type = this.getType(buffer, offset); + var atom; - this.size = view.getUint32(0); - headerOffset += 4; + if (typeof(Atom.constructorMap[type]) == 'undefined') + atom = new Atom(parent); + else + atom = new Atom.constructorMap[type](parent); + atom.parse(buffer, offset); + return atom; + }; - var typeArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.type = String.fromCharCode.apply(null, typeArrayView); - headerOffset += 4; + static getType(buffer, offset) { + // 'offset' is optional. + if (arguments.length < 2) { + offset = 0; + } + + if (buffer.byteLength - offset < this.minimumSize) + return null; + + 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; + } + + 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) - return false; - - // 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'; + + 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; + } + + return null; + }; + + getAtomsByType(type) { + if (typeof(this.childAtoms) == 'undefined') + return []; + + // 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.constructorMap = { }; + +class FileTypeAtom extends Atom { + constructor(parent) { + super(parent); + + this.description = "File Type Atom"; + this.minimumSize = 16; + this.brand = ""; + this.version = 0; + this.compatible_brands = []; } + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset, this.size); + + 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; + }; +}; + +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; + }; +}; + +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; + }; +}; + +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; + + this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; + + this.timeScale = view.getUint32(headerOffset); + headerOffset += 4; + + this.duration = view.getUint32(headerOffset); + headerOffset += 4; + + this.preferredRate = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; + + this.preferredVolume = view.getUint16(headerOffset) / (1 << 8); + 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 headerOffset; + }; +}; + +Atom.constructorMap['mvhd'] = MovieHeaderAtom.bind(null); + +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, mediaRateInteger, mediaRateFraction]); + } + + return headerOffset; + }; +}; + +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.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; + + this.trackID = 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; + + // Reserved + // An 8-byte value that is reserved for use by Apple. Set this field to 0. + headerOffset += 8; + + this.layer = view.getUint16(headerOffset); + headerOffset += 2; + + this.alternateGroup = view.getUint16(headerOffset); + headerOffset += 2; + + this.volume = view.getUint16(headerOffset) / (1 << 8); + headerOffset += 2; + + // Reserved + // A 16-bit integer that is reserved for use by Apple. Set this field to 0. + headerOffset += 2; + + 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; + + return headerOffset; + }; +}; + +Atom.constructorMap['tkhd'] = TrackHeaderAtom.bind(null); + +class MediaHeaderAtom extends FullBox { + constructor(parent) { + super(parent); + + this.description = "Media Header Atom"; + this.creationTime = 0; + this.modificationTime = 0; + this.timeScale = 0; + this.duration = 0; + this.language = 0; + this.quality = 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.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); + headerOffset += 4; + + this.timeScale = view.getUint32(headerOffset); + headerOffset += 4; + + this.duration = view.getUint32(headerOffset); + headerOffset += 4; + + this.language = view.getUint16(headerOffset); + headerOffset += 2; + + this.quality = view.getUint16(headerOffset); + headerOffset += 2; + + return headerOffset; + }; +}; + +Atom.constructorMap['mdhd'] = MediaHeaderAtom.bind(null); + +class HandlerReferenceBox extends FullBox { + constructor(parent) { + super(parent); + + this.description = 'Handler Reference Box'; + this.handlerType = ''; + this.name = ''; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + // unsigned int(32) predefined = 0; + headerOffset += 4; + + 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; + }; +}; + +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; + }; +}; + +Atom.constructorMap['stss'] = SyncSampleAtom.bind(null); + +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, + }); + }; + + 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; + }; +}; + +Atom.constructorMap['stts'] = TimeToSampleAtom.bind(null); + +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, + }); + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.sampleSize = view.getUint32(headerOffset); + headerOffset += 4; + + this.entries = view.getUint32(headerOffset); + headerOffset += 4; + + this.sampleSizes = new Uint32Array(this.entries); + var i = 0; + + while (headerOffset < this.size) { + this.sampleSizes[i] = view.getUint32(headerOffset); + headerOffset += 4; + ++i; + } + + return headerOffset; + }; +}; + +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; + }; +}; + +Atom.constructorMap['stsd'] = SampleDescriptionBox.bind(null); + +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; + return headerOffset; +} }; -Atom.prototype.getAtomByType = function(type) { - if (typeof(this.childAtoms) == 'undefined') - return null; +class AudioSampleEntry extends SampleEntry { + constructor(parent) { + super(parent); + this.description = 'Audio Sample Entry'; + this.channelCount = 0; + this.sampleSize = 0; + this.sampleRate = 0; + }; - // Bredth first - for (index in this.childAtoms) { - if (this.childAtoms[index].type == type) - return this.childAtoms[index]; - } + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - var result = null; - for (index in this.childAtoms) { - if (result = this.childAtoms[index].getAtomsByType(type)) - break; - } - return result; + // unsigned int(32)[2] reserved = 0 + headerOffset += 8; + + this.channelCount = view.getUint16(headerOffset); + headerOffset += 2; + + this.sampleSize = view.getUint16(headerOffset); + headerOffset += 2; + + // unsigned int(16) pre_defined = 0 + // const unsigned int(16) reserved = 0 + headerOffset += 4; + + this.sampleRate = (view.getUint32(headerOffset) >> 16) & 0xFFFF; + headerOffset += 4; + + 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' + } -Atom.prototype.getAtomsByType = function(type) { - if (typeof(this.childAtoms) == 'undefined') - return []; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); - var result = []; + this.descriptor = new ESDescriptor(this); + headerOffset += this.descriptor.parse(buffer, offset + headerOffset); - // Bredth first - for (index in this.childAtoms) { - if (this.childAtoms[index].type == type) - result.push(this.childAtoms[index]); + return headerOffset; } +}; - for (index in this.childAtoms) - result = result.concat(this.childAtoms[index].getAtomsByType(type)); +Atom.constructorMap['esds'] = ESDBox.bind(null); - return result; +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; + }; }; -var FileTypeAtom = function(buffer, offset) { - this.super(FileTypeAtom).constructor.call(this, buffer, offset); -} +BaseDescriptor.TagMap = { + 3: { name: 'ES_DescrTag' }, + 4: { name: 'DecoderConfigDescrTag' }, + 5: { name: 'DecSpecificInfoTag' }, +}; -FileTypeAtom.prototype = Object.create(Atom.prototype); +class ESDescriptor extends BaseDescriptor { + constructor(parent) { + super(parent); + this.description = "ES Descriptor" + this.ES_ID = 0; + }; -FileTypeAtom.prototype.setDefaults = function() { - this.super(FileTypeAtom).setDefaults.call(this); - this.minimumSize = 16; - this.brand = ""; - this.version = 0; - this.compatible_brands = []; -} + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset);; + var view = new DataView(buffer, offset); -FileTypeAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(FileTypeAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + this.ES_ID = view.getUint16(headerOffset); + headerOffset += 2; - var view = new DataView(buffer, offset, this.size); + var nextByte = view.getUint8(headerOffset); + headerOffset += 1; - var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.brand = String.fromCharCode.apply(null, brandArrayView); - headerOffset += 4; + this.streamDependencyFlag = nextByte & (1 << 7); + this.urlFlag = nextByte & (1 << 6); + this.ocrStreamFlag = nextByte & (1 << 5); + this.streamPriority = nextByte & 0x1f; - this.version = view.getUint32(headerOffset); - headerOffset += 4; + if (this.streamDependencyFlag) { + this.dependsOn_ES_Number = view.getUint16(headerOffset); + headerOffset += 2; + } - while (headerOffset < this.size - 4) { - var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); - this.compatible_brands.push(String.fromCharCode.apply(null, brandArrayView)); + if (this.urlFlag) { + var urlLength = view.getUint8(headerOffset); + headerOffset += 1; + + var array = new Uint8Array(buffer, offset + headerOffset, urlLength); + headerOffset += urlLength; + this.url = String.fromCharCode.apply(null, array); + } + + 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; + + 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; } +}; - return true; -} +class DecoderSpecificInfo extends BaseDescriptor { + constructor(parent) { + // 'Audio ISO/IEC 14496-3' && 'AudioStreamType' + if (parent.objectTypeIndication == 0x40 && parent.streamType == 0x5) + return new AudioSpecificConfig(parent); -var ContainerAtom = function(buffer, offset) { - this.super(ContainerAtom).constructor.call(this, buffer, offset); + super(parent); + this.description = 'Decoder Specific Info'; + } } -ContainerAtom.prototype = Object.create(Atom.prototype); +class AudioSpecificConfig extends BaseDescriptor { + constructor(parent) { + super(parent); + this.audioObjectType = 0; + this.samplingFrequencyIndex = 0; + this.channelConfiguration = 0; + } -ContainerAtom.prototype.setDefaults = function() { - this.super(ContainerAtom).setDefaults.call(this); - this.childAtoms = []; -} + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + if (this.size < headerOffset) + return; -ContainerAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(ContainerAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - - while (headerOffset < this.size) { - var childAtom = Atom.create(buffer, offset + headerOffset); - if (!childAtom) - break; - headerOffset += childAtom.size; - this.childAtoms.push(childAtom); - childAtom.parent = this; + 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); + + this.samplingFrequencyIndex = bitReader.readBits(4); + if (this.samplingFrequencyIndex === 0xf) + this.samplingFrequencyIndex += bitReader.readBits(24); + + return headerOffset; } } -var VersionFlagsAtom = function(buffer, offset) { - this.super(VersionFlagsAtom).constructor.call(this, 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; -VersionFlagsAtom.prototype = Object.create(Atom.prototype); + this.verticalResolution = view.getUint32(headerOffset) / (1 << 16); + headerOffset += 4; -VersionFlagsAtom.prototype.setDefaults = function() { - this.super(VersionFlagsAtom).setDefaults.call(this); - this.version = 0; - this.flags = 0; -} + // const unsigned int(32) reserved = 0 + headerOffset += 4; -VersionFlagsAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(VersionFlagsAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + this.frameCount = view.getUint16(headerOffset); + headerOffset += 2; - var view = new DataView(buffer, offset); + var array = new Uint8Array(buffer, offset + headerOffset, 32); + this.compressorName = String.fromCharCode.apply(null, array); + headerOffset += 32; - this.version = view.getUint8(headerOffset); - headerOffset += 1; + this.depth = view.getUint16(headerOffset); + headerOffset += 2; - // '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; + // int(16) pre_defined = -1; + headerOffset += 2; - return headerOffset; -} + 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; + }; +}; -var MovieHeaderAtom = function(buffer, offset) { - return this.super(MovieHeaderAtom).constructor.call(this, buffer, offset); -} +class AVCConfigurationBox extends Atom { + constructor(parent) { + super(parent); -MovieHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); - -MovieHeaderAtom.prototype.setDefaults = function() { - this.super(MovieHeaderAtom).setDefaults.call(this); - 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; -} + this.description = 'AVC Configuration Box'; + this.configurationVersion = 0; + this.AVCProfileIndication = 0; + this.profileCompatibility = 0; + this.AVCLevelIndication = 0; + this.sequenceParameterSets = []; + this.pictureParameterSets = []; + }; -MovieHeaderAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(MovieHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + parse(buffer, offset) + { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - var view = new DataView(buffer, offset); + this.configurationVersion = view.getUint8(headerOffset); + ++headerOffset; - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + this.AVCProfileIndication = view.getUint8(headerOffset); + ++headerOffset; - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + this.profileCompatibility = view.getUint8(headerOffset); + ++headerOffset; - this.timeScale = view.getUint32(headerOffset); - headerOffset += 4; + this.AVCLevelIndication = view.getUint8(headerOffset); + ++headerOffset; - this.duration = view.getUint32(headerOffset); - headerOffset += 4; + var lengthSizeMinusOne = view.getUint8(headerOffset) & 0x3; + ++headerOffset; - this.preferredRate = view.getUint32(headerOffset) / (1 << 16); - headerOffset += 4; + var numOfSequenceParameterSets = view.getUint8(headerOffset) & 0x1f; + ++headerOffset; - this.preferredVolume = view.getUint16(headerOffset) / (1 << 8); - headerOffset += 2; + for (var index = 0; index < numOfSequenceParameterSets; ++index) { + var sequenceParameterSetLength = 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.sequenceParameterSets.push(new Uint8Array(buffer, offset + headerOffset, sequenceParameterSetLength)); + headerOffset += sequenceParameterSetLength; + } -var TrackHeaderAtom = function(buffer, offset) { - this.super(TrackHeaderAtom).constructor.call(this, buffer, offset); -} + var numOfPictureParameterSets = view.getUint8(headerOffset) & 0x1f; + ++headerOffset; -TrackHeaderAtom.prototype = Object.create(Atom.prototype); - -TrackHeaderAtom.prototype.setDefaults = function() { - this.super(TrackHeaderAtom).setDefaults.call(this); - - 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; -} + for (index = 0; index < numOfPictureParameterSets; ++index) { + var pictureParameterSetLength = view.getUint16(headerOffset); + headerOffset += 2; -TrackHeaderAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(TrackHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + this.pictureParameterSets.push(new Uint8Array(buffer, offset + headerOffset, pictureParameterSetLength)); + headerOffset += pictureParameterSetLength; + } - var view = new DataView(buffer, offset); + if ([100, 110, 122, 144].indexOf(this.AVCProfileIndication) >= 0) { - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + // bit(6) reserved = '111111'b + this.chromaFormat = view.getUint8(headerOffset) & 0x3; + ++headerOffset; - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + // bit(6) reserved = '111111'b + this.bitDepthLumaMinus8 = view.getUint8(headerOffset) & 0x3; + ++headerOffset; - this.trackID = view.getUint32(headerOffset); - headerOffset += 4; + // bit(5) reserved = '11111'b;
 + this.bitDepthChromaMinus8 = view.getUint8(headerOffset) & 0x7; + ++headerOffset; + + if (headerOffset >= this.size) + return headerOffset; - // Reserved - // A 32-bit integer that is reserved for use by Apple. Set this field to 0. - headerOffset += 4; + var numOfSequenceParameterSetExt = view.getUint8(headerOffset); + this.sequenceParameterSets = []; + ++headerOffset; - this.duration = view.getUint32(headerOffset); - headerOffset += 4; + for (index = 0; index < numOfSequenceParameterSetExt; ++index) { + var sequenceParameterSetLength = view.getUint16(headerOffset); + headerOffset += 2; - // Reserved - // An 8-byte value that is reserved for use by Apple. Set this field to 0. - headerOffset += 8; + this.sequenceParameterSets.push(new Uint8Array(buffer, offset + headerOffset, sequenceParameterSetLength)); + headerOffset += sequenceParameterSetLength; + } + } - this.layer = view.getUint16(headerOffset); - headerOffset += 2; + return headerOffset; + }; +}; - this.alternateGroup = view.getUint16(headerOffset); - headerOffset += 2; +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.volume = view.getUint16(headerOffset) / (1 << 8); - headerOffset += 2; + this.general_constraint_indicator_flags = view.getUint16(headerOffset) << 32 + + view.getUint32(headerOffset + 2); + headerOffset += 6; - // Reserved - // A 16-bit integer that is reserved for use by Apple. Set this field to 0. - headerOffset += 2; + this.general_level_idc = view.getUint8(headerOffset); + ++headerOffset; - 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; -} + // bit(4) reserved = ‘1111’b; + this.min_spatial_segmentation_idc = view.getUint16(headerOffset) & 0x0FFF; + headerOffset += 2; -var MediaHeaderAtom = function(buffer, offset) { - this.super(MediaHeaderAtom).constructor.call(this, buffer, offset); -} + // bit(6) reserved = ‘111111’b; + this.parallelismType = view.getUint8(headerOffset) & 0x03; + ++headerOffset; -MediaHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); + // bit(6) reserved = ‘111111’b; + this.chromaFormat = view.getUint8(headerOffset) & 0x03; + ++headerOffset; -MediaHeaderAtom.prototype.setDefaults = function() { - this.super(MediaHeaderAtom).setDefaults.call(this); + // bit(5) reserved = ‘11111’b; + this.bitDepthLumaMinus8 = view.getUint8(headerOffset) & 0x07; + ++headerOffset; - this.creationTime = 0; - this.modificationTime = 0; - this.timeScale = 0; - this.duration = 0; - this.language = 0; - this.quality = 0; -} + // bit(5) reserved = ‘11111’b; + this.bitDepthChromaMinus8 = view.getUint8(headerOffset) & 0x07; + ++headerOffset; -MediaHeaderAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(MediaHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + this.avgFrameRate = view.getUint16(headerOffset); + headerOffset += 2; - var view = new DataView(buffer, offset); + byte = view.getUint8(headerOffset); + ++headerOffset; - this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + this.constantFrameRate = (byte & 0xC0) >> 6; + this.numTemporalLayers = (byte & 0x38) >> 3; + this.temporalIdNested = (byte & 0x04) >> 2; + this.lengthSizeMinusOne = byte & 0x02; - this.modificationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); - headerOffset += 4; + return this.size; + }; +}; - this.timeScale = view.getUint32(headerOffset); - headerOffset += 4; +Atom.constructorMap['hvcC'] = HEVCConfigurationBox.bind(null); - this.duration = view.getUint32(headerOffset); - headerOffset += 4; +class CleanApertureBox extends Atom { + constructor(parent) { + super(parent); - this.language = view.getUint16(headerOffset); - headerOffset += 2; + 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; + }; - this.quality = view.getUint16(headerOffset); - headerOffset += 2; -} + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); -var SyncSampleAtom = function(buffer, offset) { - this.super(SyncSampleAtom).constructor.call(this, 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; + + return headerOffset; + }; +}; -SyncSampleAtom.prototype = Object.create(Atom.prototype); +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; + } -SyncSampleAtom.prototype.setDefaults = function() { - this.super(SyncSampleAtom).setDefaults.call(this); + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - this.version = 0; - this.flags = 0; - this.entries = 0; - this.syncSamples = []; -} + this.trackID = view.getUint32(headerOffset); + headerOffset += 4; + + this.default_sample_description_index = view.getUint32(headerOffset); + headerOffset += 4; + + this.default_sample_duration = view.getUint32(headerOffset); + headerOffset += 4; + + 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; + }; +}; -SyncSampleAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(SyncSampleAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; +Atom.constructorMap['tenc'] = TrackEncryptionBox.bind(null); - var view = new DataView(buffer, offset); +class SegmentIndexBox extends FullBox { + constructor(parent) { + super(parent); - this.version = view.getUint8(headerOffset); - headerOffset += 1; + this.description = "Segment Index Box"; + this.referenceID = 0; + this.timeScale = 0; + this.earliestPresentationTime = 0; + this.firstOffset = 0; + this.references = []; + }; - // '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.referenceID = view.getUint32(headerOffset); + headerOffset += 4; - while (headerOffset < this.size) { - var sampleNumber = view.getUint32(headerOffset); + this.timeScale = view.getUint32(headerOffset); headerOffset += 4; - this.syncSamples.push(sampleNumber); + + if (this.version == 1) { + var upper = view.getUint32(headerOffset); + headerOffset += 4; + var lower = view.getUint32(headerOffset); + headerOffset += 4; + + this.earliestPresentationTime = (upper << 32) + lower; + + 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; } -} -var TimeToSampleAtom = function(buffer, offset) { - this.super(TimeToSampleAtom).constructor.call(this, buffer, offset); + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + this.duration = view.getUint32(headerOffset); + } } -TimeToSampleAtom.prototype = Object.create(VersionFlagsAtom.prototype); +Atom.constructorMap['mehd'] = MovieExtendsHeaderBox.bind(null); -TimeToSampleAtom.prototype.setDefaults = function() { - this.super(TimeToSampleAtom).setDefaults.call(this); - this.entries = 0; - - Object.defineProperty(this, "timeToSamples", { - value: [], - writable: true, - enumerable: false, - configurable: true, - }); -} +class MovieFragmentHeaderBox extends FullBox { + constructor(parent) { + super(parent); + this.description = 'Movie Fragment Header Box'; + this.sequenceNumber = 0; + }; -TimeToSampleAtom.prototype.parse = function(buffer, offset) { - var headerOffset = this.super(TimeToSampleAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); - var view = new DataView(buffer, offset); + this.sequenceNumber = view.getUint32(headerOffset); + headerOffset += 4; + }; +}; - this.entries = view.getUint32(headerOffset); +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; - while (headerOffset < this.size) { + 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; - var sampleDuration = view.getUint32(headerOffset); + 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); + + +class ColorBox extends Atom { + constructor(parent) { + super(parent); + this.description = "Color"; + }; + + parse(buffer, offset) { + var headerOffset = super.parse(buffer, offset); + var view = new DataView(buffer, offset); + + var typeArrayView = new Uint8Array(buffer, offset + headerOffset, 4); + this.colorType = String.fromCharCode.apply(null, typeArrayView); headerOffset += 4; - this.timeToSamples.push([sampleCount, sampleDuration]); + if (this.colorType == 'nclx') { + this.colorPrimaries = view.getUint16(headerOffset); + headerOffset += 2; + + this.transferCharacteristics = view.getUint16(headerOffset); + headerOffset += 2; + + this.matrixCoefficients = view.getUint16(headerOffset); + headerOffset += 2; + + this.fullRangeFlag = view.getUint8(headerOffset) & 0xF + headerOffset++; + } } -} \ No newline at end of file +} + +Atom.constructorMap['colr'] = ColorBox.bind(null); \ No newline at end of file