From 9114b5c6a1b71b9c3007b44d69acf3b886861a4d Mon Sep 17 00:00:00 2001 From: Jer Noble Date: Thu, 27 Mar 2014 11:46:45 -0700 Subject: [PATCH] Add new atom types: SampleSize, TrackExtends, OriginalFormat, SchemeType, and SchemeInfo. --- Atom.js | 327 ++++++++++++++++++++++++++++++++++++------------ AtomTester.html | 43 ++++--- 2 files changed, 274 insertions(+), 96 deletions(-) diff --git a/Atom.js b/Atom.js index 2fc62ef..8a73803 100644 --- a/Atom.js +++ b/Atom.js @@ -1,26 +1,34 @@ -var Atom = function(buffer, offset) { +var Atom = function (buffer, offset) { this.setDefaults(); return this.parse(buffer, offset) ? this : null; }; -Atom.create = function(buffer, offset) { +Atom.create = function (buffer, offset) { // 'offset' is optional. - if (arguments.length < 2) + if (arguments.length < 2) { offset = 0; + } if (buffer.byteLength - offset < this.minimumSize) return null; 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': @@ -33,16 +41,30 @@ Atom.create = function(buffer, offset) { 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); } -} +}; -Atom.prototype.super = function(object) { +Atom.prototype.super = function (object) { return Object.getPrototypeOf(object.prototype); -} +}; -Atom.prototype.setDefaults = function() { +Atom.prototype.setDefaults = function () { Object.defineProperty(this, "is64bit", { value: false, writable: true, @@ -61,20 +83,29 @@ Atom.prototype.setDefaults = function() { enumerable: false, configurable: true, }); + 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) { +Atom.prototype.parse = function (buffer, offset) { // 'offset' is optional. if (typeof(offset) == 'undefined') offset = 0; + this.offset = offset; + if (buffer.byteLength - offset < this.minimumSize) - return 0; + throw 'Buffer not long enough'; var view = new DataView(buffer, offset, 4); - headerOffset = 0; + var headerOffset = 0; this.size = view.getUint32(0); headerOffset += 4; @@ -86,7 +117,7 @@ Atom.prototype.parse = function(buffer, offset) { if (this.size == 1) { this.is64bit = true; if (buffer.byteLength - offset < 8) - return false; + throw 'Malformed extended size field'; // NOTE: JavaScript can only represent up to 2^53 as precise integer. // This calculation may result in incorrect values. @@ -100,61 +131,59 @@ Atom.prototype.parse = function(buffer, offset) { return headerOffset; }; -Atom.prototype.getAtomByType = function(type) { +Atom.prototype.getAtomByType = function (type) { if (typeof(this.childAtoms) == 'undefined') return null; // Bredth first - for (index in this.childAtoms) { + for (var index in this.childAtoms) { if (this.childAtoms[index].type == type) return this.childAtoms[index]; } var result = null; - for (index in this.childAtoms) { - if (result = this.childAtoms[index].getAtomsByType(type)) + for (var index in this.childAtoms) { + if (result = this.childAtoms[index].getAtomByType(type)) break; } return result; }; -Atom.prototype.getAtomsByType = function(type) { +Atom.prototype.getAtomsByType = function (type) { if (typeof(this.childAtoms) == 'undefined') return []; var result = []; // Bredth first - for (index in this.childAtoms) { + for (var index in this.childAtoms) { if (this.childAtoms[index].type == type) result.push(this.childAtoms[index]); } - for (index in this.childAtoms) + for (var index in this.childAtoms) result = result.concat(this.childAtoms[index].getAtomsByType(type)); return result; }; -var FileTypeAtom = function(buffer, offset) { +var FileTypeAtom = function (buffer, offset) { this.super(FileTypeAtom).constructor.call(this, buffer, offset); } FileTypeAtom.prototype = Object.create(Atom.prototype); -FileTypeAtom.prototype.setDefaults = function() { +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 = []; } -FileTypeAtom.prototype.parse = function(buffer, offset) { +FileTypeAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(FileTypeAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset, this.size); var brandArrayView = new Uint8Array(buffer, offset + headerOffset, 4); @@ -173,22 +202,21 @@ FileTypeAtom.prototype.parse = function(buffer, offset) { return true; } -var ContainerAtom = function(buffer, offset) { - this.super(ContainerAtom).constructor.call(this, buffer, offset); +var ContainerAtom = function (buffer, offset, description) { + var atom = this.super(ContainerAtom).constructor.call(this, buffer, offset); + atom.description = description; + return; } ContainerAtom.prototype = Object.create(Atom.prototype); -ContainerAtom.prototype.setDefaults = function() { +ContainerAtom.prototype.setDefaults = function () { this.super(ContainerAtom).setDefaults.call(this); this.childAtoms = []; } -ContainerAtom.prototype.parse = function(buffer, offset) { +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) @@ -197,25 +225,23 @@ ContainerAtom.prototype.parse = function(buffer, offset) { this.childAtoms.push(childAtom); childAtom.parent = this; } + return headerOffset; } -var VersionFlagsAtom = function(buffer, offset) { +var VersionFlagsAtom = function (buffer, offset) { this.super(VersionFlagsAtom).constructor.call(this, buffer, offset); } VersionFlagsAtom.prototype = Object.create(Atom.prototype); -VersionFlagsAtom.prototype.setDefaults = function() { +VersionFlagsAtom.prototype.setDefaults = function () { this.super(VersionFlagsAtom).setDefaults.call(this); this.version = 0; this.flags = 0; } -VersionFlagsAtom.prototype.parse = function(buffer, offset) { +VersionFlagsAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(VersionFlagsAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.version = view.getUint8(headerOffset); @@ -228,14 +254,15 @@ VersionFlagsAtom.prototype.parse = function(buffer, offset) { return headerOffset; } -var MovieHeaderAtom = function(buffer, offset) { +var MovieHeaderAtom = function (buffer, offset) { return this.super(MovieHeaderAtom).constructor.call(this, buffer, offset); } MovieHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); -MovieHeaderAtom.prototype.setDefaults = function() { +MovieHeaderAtom.prototype.setDefaults = function () { this.super(MovieHeaderAtom).setDefaults.call(this); + this.description = "Movie Header Atom"; this.creationTime = 0; this.modificationTime = 0; this.timeScale = 0; @@ -250,11 +277,8 @@ MovieHeaderAtom.prototype.setDefaults = function() { this.nextTrackID = 0; } -MovieHeaderAtom.prototype.parse = function(buffer, offset) { +MovieHeaderAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(MovieHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); @@ -328,15 +352,16 @@ MovieHeaderAtom.prototype.parse = function(buffer, offset) { return true; } -var TrackHeaderAtom = function(buffer, offset) { +var TrackHeaderAtom = function (buffer, offset) { this.super(TrackHeaderAtom).constructor.call(this, buffer, offset); } TrackHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); -TrackHeaderAtom.prototype.setDefaults = function() { +TrackHeaderAtom.prototype.setDefaults = function () { this.super(TrackHeaderAtom).setDefaults.call(this); + this.description = "Track Header Atom"; this.creationTime = 0; this.modificationTime = 0; this.trackID = 0; @@ -349,11 +374,8 @@ TrackHeaderAtom.prototype.setDefaults = function() { this.height = 0; } -TrackHeaderAtom.prototype.parse = function(buffer, offset) { +TrackHeaderAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(TrackHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); @@ -424,15 +446,16 @@ TrackHeaderAtom.prototype.parse = function(buffer, offset) { headerOffset += 4; } -var MediaHeaderAtom = function(buffer, offset) { +var MediaHeaderAtom = function (buffer, offset) { this.super(MediaHeaderAtom).constructor.call(this, buffer, offset); } MediaHeaderAtom.prototype = Object.create(VersionFlagsAtom.prototype); -MediaHeaderAtom.prototype.setDefaults = function() { +MediaHeaderAtom.prototype.setDefaults = function () { this.super(MediaHeaderAtom).setDefaults.call(this); + this.description = "Media Header Atom"; this.creationTime = 0; this.modificationTime = 0; this.timeScale = 0; @@ -441,11 +464,8 @@ MediaHeaderAtom.prototype.setDefaults = function() { this.quality = 0; } -MediaHeaderAtom.prototype.parse = function(buffer, offset) { +MediaHeaderAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(MediaHeaderAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.creationTime = new Date(view.getUint32(headerOffset)*1000 + Date.UTC(1904, 0, 1)); @@ -465,28 +485,26 @@ MediaHeaderAtom.prototype.parse = function(buffer, offset) { this.quality = view.getUint16(headerOffset); headerOffset += 2; -} +}; -var SyncSampleAtom = function(buffer, offset) { +var SyncSampleAtom = function (buffer, offset) { this.super(SyncSampleAtom).constructor.call(this, buffer, offset); -} +}; SyncSampleAtom.prototype = Object.create(Atom.prototype); -SyncSampleAtom.prototype.setDefaults = function() { +SyncSampleAtom.prototype.setDefaults = function () { this.super(SyncSampleAtom).setDefaults.call(this); + this.description = "Sync Sample Atom"; this.version = 0; this.flags = 0; this.entries = 0; this.syncSamples = []; -} +}; -SyncSampleAtom.prototype.parse = function(buffer, offset) { +SyncSampleAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(SyncSampleAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.version = view.getUint8(headerOffset); @@ -499,41 +517,45 @@ SyncSampleAtom.prototype.parse = function(buffer, offset) { 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.push(sampleNumber); + this.syncSamples[i] = sampleNumber; + ++i; } -} +}; -var TimeToSampleAtom = function(buffer, offset) { +var TimeToSampleAtom = function (buffer, offset) { this.super(TimeToSampleAtom).constructor.call(this, buffer, offset); -} +}; TimeToSampleAtom.prototype = Object.create(VersionFlagsAtom.prototype); -TimeToSampleAtom.prototype.setDefaults = function() { +TimeToSampleAtom.prototype.setDefaults = function () { this.super(TimeToSampleAtom).setDefaults.call(this); + this.description = "Time-to-Sample Atom"; this.entries = 0; Object.defineProperty(this, "timeToSamples", { - value: [], + value: null, writable: true, enumerable: false, configurable: true, }); } -TimeToSampleAtom.prototype.parse = function(buffer, offset) { +TimeToSampleAtom.prototype.parse = function (buffer, offset) { var headerOffset = this.super(TimeToSampleAtom).parse.call(this, buffer, offset); - if (!headerOffset) - return 0; - var view = new DataView(buffer, offset); this.entries = view.getUint32(headerOffset); - headerOffset += 4; + headerOffset += 4; + + this.timeToSamples = new Array(this.entries); + var i = 0; while (headerOffset < this.size) { var sampleCount = view.getUint32(headerOffset); @@ -542,6 +564,155 @@ TimeToSampleAtom.prototype.parse = function(buffer, offset) { var sampleDuration = view.getUint32(headerOffset); headerOffset += 4; - this.timeToSamples.push([sampleCount, sampleDuration]); + this.timeToSamples[i] = [sampleCount, sampleDuration]; + ++i; + } +} + +var SampleSizeAtom = function (buffer, offset) { + this.super(SampleSizeAtom).constructor.call(this, 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, + }); +} + +SampleSizeAtom.prototype.parse = function (buffer, offset) { + var headerOffset = this.super(SampleSizeAtom).parse.call(this, 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; + } +} + +var TrackExtendsAtom = function (buffer, offset) { + this.super(TrackExtendsAtom).constructor.call(this, buffer, offset); +} + +TrackExtendsAtom.prototype = Object.create(VersionFlagsAtom.prototype); + +TrackExtendsAtom.prototype.setDefaults = function () { + this.super(TrackExtendsAtom).setDefaults.call(this); + + 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; +} + +TrackExtendsAtom.prototype.parse = function (buffer, offset) { + var headerOffset = this.super(TrackExtendsAtom).parse.call(this, buffer, offset); + var view = new DataView(buffer, offset); + + 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; +} + +var OriginalFormatBox = function (buffer, offset) { + this.super(OriginalFormatBox).constructor.call(this, buffer, offset); +} + +OriginalFormatBox.prototype = Object.create(Atom.prototype); + +OriginalFormatBox.prototype.setDefaults = function () { + this.super(OriginalFormatBox).setDefaults.call(this); + + this.description = "Original Format Box"; + this.dataFormat = 0; +} + +OriginalFormatBox.prototype.parse = function (buffer, offset) { + var headerOffset = this.super(OriginalFormatBox).parse.call(this, buffer, offset); + var view = new DataView(buffer, offset); + + this.dataFormat = view.getUint32(headerOffset); + headerOffset += 4; +} + +var SchemeTypeBox = function (buffer, offset) { + this.super(SchemeTypeBox).constructor.call(this, buffer, offset); +} + +SchemeTypeBox.prototype = Object.create(VersionFlagsAtom.prototype); + +SchemeTypeBox.prototype.setDefaults = function () { + this.super(SchemeTypeBox).setDefaults.call(this); + + this.description = "Scheme Type Box"; + this.schemeType = 0; + this.schemeVersion = 0; + this.schemeURL = 0; +} + +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); } -} \ No newline at end of file +} + +var SchemeInfoBox = function (buffer, offset) { + this.super(SchemeInfoBox).constructor.call(this, buffer, offset); +} + +SchemeInfoBox.prototype = Object.create(VersionFlagsAtom.prototype); + +SchemeInfoBox.prototype.setDefaults = function () { + this.super(SchemeInfoBox).setDefaults.call(this); + + this.description = "Scheme Information Box"; + this.schemeSpecificData = 0; +} + +SchemeInfoBox.prototype.parse = function (buffer, offset) { + var headerOffset = this.super(SchemeInfoBox).parse.call(this, buffer, offset); + this.schemeSpecificData = buffer.slice(headerOffset, this.size - headerOffset); +} + + diff --git a/AtomTester.html b/AtomTester.html index 2617264..78587ca 100644 --- a/AtomTester.html +++ b/AtomTester.html @@ -13,39 +13,38 @@ var reader = new FileReader(); output.innerHTML = ''; - function checkForMoovAtom(offset) { + function checkForAtom(offset) { reader.onload = function(e) { - var result = e.target.result; - var basicAtom = new Atom(result); - if (!basicAtom) - return; - else if (basicAtom.type == 'moov') - readMoovAtom(offset, basicAtom.size); - else - checkForMoovAtom(offset + basicAtom.size); + var atom = new Atom(e.target.result); + if (atom) + readAtom(offset, atom.size); }; var subset = file.slice(offset, offset + 16); reader.readAsArrayBuffer(subset); - }; - - function readMoovAtom(offset, length) { - reader.onload = function(e) { - moovAtom = Atom.create(e.target.result); + } + + function readAtom(offset, length) { + reader.onload = function(e) { + atom = Atom.create(e.target.result); + if (!atom) + return; var output = document.getElementById('output'); - output.appendChild(toDOMNode(moovAtom)); + output.appendChild(toDOMNode(atom)); + if (file.size > offset + length) + checkForAtom(offset + length); }; var subset = file.slice(offset, offset + length); reader.readAsArrayBuffer(subset); - }; + } - checkForMoovAtom(0); + checkForAtom(0); } function toDOMRepresentation(object) { if (object instanceof Atom) return toDOMNode(object); - else if (object instanceof Array) { + else if (object instanceof Array || object instanceof Uint32Array) { var span = document.createElement('span'); span.appendChild(document.createTextNode('[')); for (var i = 0; i < object.length; ++i) { @@ -67,6 +66,13 @@ function toDOMNode(object) { var output = document.createElement('dl'); + + if (typeof(object.description) == 'string') { + var div = document.createElement('div'); + div.classList.add('description'); + div.appendChild(document.createTextNode(object.description)); + output.appendChild(div); + } for (property in object) { var value = object[property]; @@ -98,6 +104,7 @@ dt, dd { display: inline-block; min-width: 8em; } dd { margin-left: 1em; } + .description { text-align: center; font-weight: bold; } /* make the output display as a table */ /* -- 2.40.1