var mediaRateFraction = view.getUint16(headerOffset);
headerOffset += 2;
- this.edits.push([segmentDuration, mediaTime, mediaRateFraction, mediaRateInteger]);
+ this.edits.push([segmentDuration, mediaTime, mediaRateInteger, mediaRateFraction]);
}
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;
+
+ 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++;
+ }
+ }
+}
+
+Atom.constructorMap['colr'] = ColorBox.bind(null);
\ No newline at end of file
continue;
var div = document.createElement('div');
var dt = document.createElement('dt');
- dt.appendChild(document.createTextNode(property));
+ dt.appendChild(document.createTextNode(property + ': '));
var dd = document.createElement('dd');
dd.appendChild(toDOMRepresentation(value));
div.appendChild(dt);
<style>
dl { border: 1px solid black; padding: 1px; }
dt, dd { display: inline; }
- dt:after { content:": " }
+ /*dt:after { content:": " }*/
dd:after { content:"\A"; white-space:pre; }
dd dl { margin: 0; vertical-align: top }
dd span { display: inline-block; vertical-align: top; }
--- /dev/null
+<!DOCTYPE html>
+<head>
+ <title>GapFinder</title>
+ <script src=Atom.js></script>
+ <script>
+ // Add Blob.prototype.slice if it does not already exist:
+ if (typeof(Blob.prototype.slice) == 'undefined' && typeof(Blob.prototype.webkitSlice) != 'undefined')
+ Blob.prototype.slice = Blob.prototype.webkitSlice;
+
+ function readAtoms(file, offset, length) {
+ function tryToReadAtom(file, offset) {
+ return new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.onload = (event) => {
+ var atom = new Atom();
+ atom.parse(reader.result, 0);
+ resolve(atom);
+ };
+ reader.onerror = (error) => {
+ reject(error);
+ };
+ var subset = file.slice(offset, offset + 36);
+ reader.readAsArrayBuffer(subset);
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ var atoms = [];
+ var offset = 0;
+ tryToReadAtom(file, offset).then((atom) => {
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var atom = Atom.create(reader.result);
+ resolve(atom);
+ };
+ reader.onerror = (error) => {
+ reject(error);
+ };
+ var subset = file.slice(offset, offset + length);
+ reader.readAsArrayBuffer(subset);
+ }, (error) {
+ reject(error);
+ })
+ })
+ }
+
+
+
+ function onFileSelect(e) {
+ var file = e.target.files[0];
+
+
+ readAtom(file, 0, file.size).then((atom) => {
+ console.log(atom);
+ }, () => {
+ // do some failed stuff
+ });
+ }
+
+ function setup() {
+ document.getElementById('file').addEventListener('change', onFileSelect, false);
+ }
+ </script>
+ <style>
+ dl { border: 1px solid black; padding: 1px; }
+ dt, dd { display: inline; }
+ dt:after { content:": " }
+ dd:after { content:"\A"; white-space:pre; }
+ dd dl { margin: 0; vertical-align: top }
+ dd span { display: inline-block; vertical-align: top; }
+
+ 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 */
+ /*
+ dl { display: table; }
+ dl div { display: table-row; }
+ dt, dd { display: table-cell; }
+ */
+ </style>
+</head>
+<body id="atomtester" onload="setup()">
+ <input type="file" id="file" name="file" />
+ <div id=output>
+</body>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <title>GenerateManifest</title>
+ <script src=Atom.js></script>
+ <script>
+ // Add Blob.prototype.slice if it does not already exist:
+ if (typeof(Blob.prototype.slice) == 'undefined' && typeof(Blob.prototype.webkitSlice) != 'undefined')
+ Blob.prototype.slice = Blob.prototype.webkitSlice;
+
+ function onFileSelect(e) {
+ atoms = [];
+ var file = e.target.files[0];
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+ reader.addEventListener('load', (e) => { onload(e, file); });
+ }
+
+ function onload(e, file) {
+ var reader = e.target;
+ var offset = 0;
+ while (offset < reader.result.byteLength) {
+ var atom = Atom.create(reader.result, offset);
+ offset += atom.size;
+ atoms.push(atom);
+ }
+
+ var manifest = generateManifest(atoms, file);
+ document.body.appendChild(document.createTextNode(JSON.stringify(manifest)));
+ }
+
+ function generateManifest(atoms, file)
+ {
+ var manifest = new Object;
+ manifest.url = file.name;
+
+ manifest.type = file.type;
+ var codecs = [];
+
+ var moov = atoms.find((atom) => { return atom.type === 'moov'; });
+ if (!moov)
+ return;
+
+ var timeScale = moov.getAtomByType('mdhd').timeScale
+
+ manifest.init = { offset: 0, size: moov.offset + moov.size };
+
+ var stsds = moov.getAtomsByType('stsd');
+ stsds.forEach((stsd) => {
+ stsd.getAtomsByType('avc1').forEach((avc1) => {
+ avc1.getAtomsByType('avcC').forEach((avcC) => {
+ var profile = ('0' + avcC.AVCProfileIndication.toString(16)).substr(-2);
+ var compat = ('0' + avcC.profileCompatibility.toString(16)).substr(-2);
+ var level = ('0' + avcC.AVCLevelIndication.toString(16)).substr(-2);
+ codecs.push(`"avc1.${profile + compat + level}"`);
+ });
+ });
+ stsd.getAtomsByType('hvc1').forEach((hvc1) => {
+ codecs.push(`"hvc1"`);
+ });
+ });
+
+ if (codecs.length)
+ manifest.type += `; codecs=${ codecs.join(',') }`;
+
+ manifest.media = [];
+ var trex = moov.getAtomsByType('trex')[0];
+
+ atoms.forEach((atom, index) => {
+ if (atom.type !== 'moof')
+ return;
+
+ var offset = atom.offset;
+ var size = atom.size;
+ var mdat = atoms[index + 1];
+ var timestamp = 0;
+ if (mdat.type === 'mdat')
+ size += mdat.size;
+ var tfdt = atom.getAtomByType('tfdt');
+ if (tfdt)
+ timestamp = tfdt.baseMediaDecodeTime / timeScale;
+
+ var defaultSampleDuration = trex.default_sample_duration;
+
+ var tfhd = atom.getAtomByType('tfhd');
+ if (tfhd.defaultSampleDurationPresent)
+ defaultSampleDuration = tfhd.defaultSampleDuration;
+
+ var totalDuration = 0;
+ var trun = atom.getAtomByType('trun');
+ trun.samples.forEach((sample) => {
+ if (trun.sampleDurationPresent)
+ totalDuration += sample.sampleDuration;
+ else
+ totalDuration += defaultSampleDuration;
+ });
+ var duration = totalDuration / timeScale;
+
+ manifest.media.push({ offset: offset, size: size, timestamp: timestamp, duration: duration });
+ });
+ return manifest;
+ }
+
+ function setup() {
+ document.getElementById('file').addEventListener('change', onFileSelect, false);
+ }
+ </script>
+</head>
+<body onload="setup()">
+ <input type="file" id="file" name="file" />
+ <div id=output></div>
+</body>
\ No newline at end of file
--- /dev/null
+var SampleData = function() {
+ "use strict";
+ this.maximumSampleSize = 0;
+ this.maxScenes = 0;
+ this.syncSampleSizes = [];
+ this.decisions = [];
+ this.thresholds = [];
+ this.syncSampleTimes = [];
+ this.sceneSamples = [];
+};
+
+SampleData.prototype.decision = function(data, k, n, count) {
+ "use strict";
+ var n_m_1 = (n - 1 >= 0 ? n - 1 : 0);
+ var n_m_2 = (n - 2 >= 0 ? n - 2 : 0);
+ var n_p_1 = (n + 1 < count ? n + 1 : count - 1);
+
+ if (
+ ((
+ (data[n_m_1] - data[n_m_2] > 0) &&
+ (data[n] - data[n_m_1] > 0) &&
+ (data[n_p_1] - data[n] > 0) &&
+ (data[n_p_1] - data[n] >= data[n] - data[n - 1])
+ )
+ ||
+ (
+ (data[n_m_1] - data[n_m_2] < 0) &&
+ (data[n] - data[n_m_1] < 0) &&
+ (data[n_p_1] - data[n] < 0) &&
+ (data[n_p_1] - data[n] <= data[n] - data[n - 1])
+ )))
+ return 0;
+
+ var directionSet = new Array(4);
+ directionSet[0] = Math.abs(data[n] - data[n_m_1]);
+ directionSet[1] = Math.abs(data[n] - data[n_m_2]);
+ directionSet[2] = Math.abs(data[n_p_1] - data[n_m_1]);
+ directionSet[3] = Math.abs(data[n_p_1] - data[n_m_2]);
+
+ return directionSet.sort()[0];
+};
+
+SampleData.prototype.threshold = function(data, distances, k, n, count) {
+ "use strict";
+ var mean = 0;
+ var S = 0;
+ var min, max, min_n1, max_n1;
+ var directionSet = new Array(4);
+
+ min = min_n1 = data[k];
+ max = max_n1 = data[k];
+
+ var n_m_1 = (n >= 1 ? n - 1 : 0);
+ var n_m_2 = (n >= 2 ? n - 2 : 0);
+ var n_p_1 = (n + 1 < count ? n + 1 : count - 1);
+ directionSet[0] = data[n] - data[n_m_1];
+ directionSet[1] = data[n] - data[n_m_2];
+ directionSet[2] = data[n_p_1] - data[n_m_1];
+ directionSet[3] = data[n_p_1] - data[n_m_2];
+
+ // Knuth Variance Algorithm with Min/Max
+ // http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Algorithm_III
+ for (var i = k, count = 1; i <= n; ++i, ++count) {
+ var x = data[i];
+ var delta = x - mean;
+ mean += delta / count;
+ S += delta * (x - mean);
+
+ // Caculate min/max and min/max excluding the last entry.
+ if (x > max) max = x;
+ if (x < min) min = x;
+ if (i != n) {
+ if (x > max_n1) max_n1 = x;
+ if (x < max_n1) min_n1 = x;
+ }
+ }
+
+ var distance = Math.sqrt(1 / Math.log(distances[n] - distances[k]));
+ var variance = S / ((n - k) - 1);
+ var stddev = Math.sqrt(variance);
+ var weightedDev = stddev / (max - min);
+ var eta = 0;
+
+ var k_m_1 = (k >= 1 ? k - 1 : 0);
+
+ if (directionSet.sort()[0] > 0) {
+ eta = Math.abs(data[k] - data[k_m_1]);
+ if (max_n1 > data[k]) eta += Math.abs(max_n1 - data[k]);
+ } else {
+ eta = Math.abs(data[k] - data[k_m_1]);
+ if (min_n1 < data[k]) eta += Math.abs(min_n1 - data[k]);
+ }
+
+ return distance * weightedDev * eta;
+};
+
+SampleData.prototype.load = function(file, completion) {
+ "use strict";
+ this.reader = new FileReader();
+ this.file = file;
+ this.completion = completion;
+ this.checkForMoovAtom(0);
+};
+
+SampleData.prototype.checkForMoovAtom = function(offset) {
+ "use strict";
+ this.reader.onload = (function(e) {
+ var result = e.target.result;
+ var basicAtom = new Atom();
+ basicAtom.parse(result);
+
+ if (basicAtom.type == 'moov')
+ this.readMoovAtom(offset, basicAtom.size);
+ else
+ this.checkForMoovAtom(offset + basicAtom.size);
+ }).bind(this);
+ var subset = this.file.slice(offset, offset + 16);
+ this.reader.readAsArrayBuffer(subset);
+};
+
+SampleData.prototype.readMoovAtom = function(offset, length) {
+ "use strict";
+ this.reader.onload = (function(e) {
+ var moovAtom = Atom.create(e.target.result);
+ var mediaAtoms = moovAtom.getAtomsByType('mdia');
+ var mediaAtom = mediaAtoms.find(function(atom){
+ return atom.getAtomByType('hdlr').handlerType == 'vide';
+ });
+ var syncSampleAtom = mediaAtom.getAtomByType('stss');
+ var sampleSizeAtom = syncSampleAtom.parent.getAtomByType('stsz');
+ var mediaHeaderAtom = syncSampleAtom.parent.parent.parent.getAtomByType('mdhd');
+ this.timeScale = mediaHeaderAtom.timeScale;
+ for (var i = 0; i < syncSampleAtom.syncSamples.length; ++i) {
+ var sampleNumber = syncSampleAtom.syncSamples[i];
+ var sampleSize = sampleSizeAtom.sampleSizes[sampleNumber];
+ this.syncSampleSizes.push(sampleSize);
+ if (this.maximumSampleSize < sampleSize)
+ this.maximumSampleSize = sampleSize;
+
+ var sampleTimeAtom = syncSampleAtom.parent.getAtomByType('stts');
+ var sampleSum = 0;
+
+
+ this.syncSampleTimes.push(timeSum);
+ }
+
+ this.sceneSamples.push([0, 0]);
+ for (var i = 1; i < this.syncSampleTimes.length; ++i) {
+ var k = this.sceneSamples[this.sceneSamples.length-1][1];
+ var n = i;
+ var T = this.threshold(this.syncSampleSizes, this.syncSampleTimes, k, n, this.syncSampleSizes.length);
+ var D = this.decision(this.syncSampleSizes, k, n, this.syncSampleSizes.length);
+ this.thresholds.push(isFinite(T) ? T : 0);
+ this.decisions.push(D);
+
+ if (D > T && k + 1 < n) {
+ this.sceneSamples.push([D - T, i]);
+ }
+ }
+
+ this.sceneSamples.sort(function(a, b) {
+ if (a[0] == b[0])
+ return a[1] - b[1]
+ return a[0] - b[0];
+ });
+
+ this.completion();
+
+ }).bind(this);
+ var subset = this.file.slice(offset, offset + length);
+ this.reader.readAsArrayBuffer(subset);
+};
--- /dev/null
+<!DOCTYPE html>
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <title>SceneFinder</title>
+ <script src=Atom.js></script>
+ <script src=SampleData.js></script>
+ <script>
+ // Add Blob.prototype.slice if it does not already exist:
+ if (typeof(Blob.prototype.slice) == 'undefined' && typeof(Blob.prototype.webkitSlice) != 'undefined')
+ Blob.prototype.slice = Blob.prototype.webkitSlice;
+
+ var sampleData = null;
+
+ function onFileSelect(e) {
+ var file = e.target.files[0];
+ output.innerHTML = '';
+ sampleData = new SampleData();
+ sampleData.load(file, function(){
+ var video = document.createElement('video');
+ var url = window.URL.createObjectURL(file);
+ video.src = url;
+ video.controls = true;
+ output.appendChild(video);
+
+ var ol = document.createElement('ol');
+ output.appendChild(ol);
+ for (var i = 0; i < sampleData.sceneSamples.length; ++i) {
+ var li = document.createElement('li');
+ ol.appendChild(li);
+
+ var a = document.createElement('a');
+ li.appendChild(a);
+ a.href = '#';
+ a.time = sampleData.sceneSamples[i][0];
+ a.onclick = function(e) {
+ video.currentTime = e.target.time;
+ };
+ a.innerText = 'Chapter ' + i;
+ }
+ });
+ }
+
+ function setup() {
+ document.getElementById('file').addEventListener('change', onFileSelect, false);
+ }
+ </script>
+</head>
+<body onload="setup()">
+ <input type="file" id="file" name="file" />
+ <div id=output></div>
+</body>
\ No newline at end of file