You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
977 lines
30 KiB
JavaScript
977 lines
30 KiB
JavaScript
module.exports = (function(){
|
|
|
|
'use strict';
|
|
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
error(message);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Represents a 2-dimensional size value.
|
|
*/
|
|
var Size = (function size() {
|
|
function constructor(w, h) {
|
|
this.w = w;
|
|
this.h = h;
|
|
}
|
|
constructor.prototype = {
|
|
toString: function () {
|
|
return "(" + this.w + ", " + this.h + ")";
|
|
},
|
|
getHalfSize: function() {
|
|
return new Size(this.w >>> 1, this.h >>> 1);
|
|
},
|
|
length: function() {
|
|
return this.w * this.h;
|
|
}
|
|
};
|
|
return constructor;
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
var Bytestream = (function BytestreamClosure() {
|
|
function constructor(arrayBuffer, start, length) {
|
|
this.bytes = new Uint8Array(arrayBuffer);
|
|
this.start = start || 0;
|
|
this.pos = this.start;
|
|
this.end = (start + length) || this.bytes.length;
|
|
}
|
|
constructor.prototype = {
|
|
get length() {
|
|
return this.end - this.start;
|
|
},
|
|
get position() {
|
|
return this.pos;
|
|
},
|
|
get remaining() {
|
|
return this.end - this.pos;
|
|
},
|
|
readU8Array: function (length) {
|
|
if (this.pos > this.end - length)
|
|
return null;
|
|
var res = this.bytes.subarray(this.pos, this.pos + length);
|
|
this.pos += length;
|
|
return res;
|
|
},
|
|
readU32Array: function (rows, cols, names) {
|
|
cols = cols || 1;
|
|
if (this.pos > this.end - (rows * cols) * 4)
|
|
return null;
|
|
if (cols == 1) {
|
|
var array = new Uint32Array(rows);
|
|
for (var i = 0; i < rows; i++) {
|
|
array[i] = this.readU32();
|
|
}
|
|
return array;
|
|
} else {
|
|
var array = new Array(rows);
|
|
for (var i = 0; i < rows; i++) {
|
|
var row = null;
|
|
if (names) {
|
|
row = {};
|
|
for (var j = 0; j < cols; j++) {
|
|
row[names[j]] = this.readU32();
|
|
}
|
|
} else {
|
|
row = new Uint32Array(cols);
|
|
for (var j = 0; j < cols; j++) {
|
|
row[j] = this.readU32();
|
|
}
|
|
}
|
|
array[i] = row;
|
|
}
|
|
return array;
|
|
}
|
|
},
|
|
read8: function () {
|
|
return this.readU8() << 24 >> 24;
|
|
},
|
|
readU8: function () {
|
|
if (this.pos >= this.end)
|
|
return null;
|
|
return this.bytes[this.pos++];
|
|
},
|
|
read16: function () {
|
|
return this.readU16() << 16 >> 16;
|
|
},
|
|
readU16: function () {
|
|
if (this.pos >= this.end - 1)
|
|
return null;
|
|
var res = this.bytes[this.pos + 0] << 8 | this.bytes[this.pos + 1];
|
|
this.pos += 2;
|
|
return res;
|
|
},
|
|
read24: function () {
|
|
return this.readU24() << 8 >> 8;
|
|
},
|
|
readU24: function () {
|
|
var pos = this.pos;
|
|
var bytes = this.bytes;
|
|
if (pos > this.end - 3)
|
|
return null;
|
|
var res = bytes[pos + 0] << 16 | bytes[pos + 1] << 8 | bytes[pos + 2];
|
|
this.pos += 3;
|
|
return res;
|
|
},
|
|
peek32: function (advance) {
|
|
var pos = this.pos;
|
|
var bytes = this.bytes;
|
|
if (pos > this.end - 4)
|
|
return null;
|
|
var res = bytes[pos + 0] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3];
|
|
if (advance) {
|
|
this.pos += 4;
|
|
}
|
|
return res;
|
|
},
|
|
read32: function () {
|
|
return this.peek32(true);
|
|
},
|
|
readU32: function () {
|
|
return this.peek32(true) >>> 0;
|
|
},
|
|
read4CC: function () {
|
|
var pos = this.pos;
|
|
if (pos > this.end - 4)
|
|
return null;
|
|
var res = "";
|
|
for (var i = 0; i < 4; i++) {
|
|
res += String.fromCharCode(this.bytes[pos + i]);
|
|
}
|
|
this.pos += 4;
|
|
return res;
|
|
},
|
|
readFP16: function () {
|
|
return this.read32() / 65536;
|
|
},
|
|
readFP8: function () {
|
|
return this.read16() / 256;
|
|
},
|
|
readISO639: function () {
|
|
var bits = this.readU16();
|
|
var res = "";
|
|
for (var i = 0; i < 3; i++) {
|
|
var c = (bits >>> (2 - i) * 5) & 0x1f;
|
|
res += String.fromCharCode(c + 0x60);
|
|
}
|
|
return res;
|
|
},
|
|
readUTF8: function (length) {
|
|
var res = "";
|
|
for (var i = 0; i < length; i++) {
|
|
res += String.fromCharCode(this.readU8());
|
|
}
|
|
return res;
|
|
},
|
|
readPString: function (max) {
|
|
var len = this.readU8();
|
|
assert (len <= max);
|
|
var res = this.readUTF8(len);
|
|
this.reserved(max - len - 1, 0);
|
|
return res;
|
|
},
|
|
skip: function (length) {
|
|
this.seek(this.pos + length);
|
|
},
|
|
reserved: function (length, value) {
|
|
for (var i = 0; i < length; i++) {
|
|
assert (this.readU8() == value);
|
|
}
|
|
},
|
|
seek: function (index) {
|
|
if (index < 0 || index > this.end) {
|
|
error("Index out of bounds (bounds: [0, " + this.end + "], index: " + index + ").");
|
|
}
|
|
this.pos = index;
|
|
},
|
|
subStream: function (start, length) {
|
|
return new Bytestream(this.bytes.buffer, start, length);
|
|
}
|
|
};
|
|
return constructor;
|
|
})();
|
|
|
|
|
|
var PARANOID = true; // Heavy-weight assertions.
|
|
|
|
/**
|
|
* Reads an mp4 file and constructs a object graph that corresponds to the box/atom
|
|
* structure of the file. Mp4 files are based on the ISO Base Media format, which in
|
|
* turn is based on the Apple Quicktime format. The Quicktime spec is available at:
|
|
* http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF. An mp4 spec
|
|
* also exists, but I cannot find it freely available.
|
|
*
|
|
* Mp4 files contain a tree of boxes (or atoms in Quicktime). The general structure
|
|
* is as follows (in a pseudo regex syntax):
|
|
*
|
|
* Box / Atom Structure:
|
|
*
|
|
* [size type [version flags] field* box*]
|
|
* <32> <4C> <--8--> <24-> <-?-> <?>
|
|
* <------------- box size ------------>
|
|
*
|
|
* The box size indicates the entire size of the box and its children, we can use it
|
|
* to skip over boxes that are of no interest. Each box has a type indicated by a
|
|
* four character code (4C), this describes how the box should be parsed and is also
|
|
* used as an object key name in the resulting box tree. For example, the expression:
|
|
* "moov.trak[0].mdia.minf" can be used to access individual boxes in the tree based
|
|
* on their 4C name. If two or more boxes with the same 4C name exist in a box, then
|
|
* an array is built with that name.
|
|
*
|
|
*/
|
|
var MP4Reader = (function reader() {
|
|
var BOX_HEADER_SIZE = 8;
|
|
var FULL_BOX_HEADER_SIZE = BOX_HEADER_SIZE + 4;
|
|
|
|
function constructor(stream) {
|
|
this.stream = stream;
|
|
this.tracks = {};
|
|
}
|
|
|
|
constructor.prototype = {
|
|
readBoxes: function (stream, parent) {
|
|
while (stream.peek32()) {
|
|
var child = this.readBox(stream);
|
|
if (child.type in parent) {
|
|
var old = parent[child.type];
|
|
if (!(old instanceof Array)) {
|
|
parent[child.type] = [old];
|
|
}
|
|
parent[child.type].push(child);
|
|
} else {
|
|
parent[child.type] = child;
|
|
}
|
|
}
|
|
},
|
|
readBox: function readBox(stream) {
|
|
var box = { offset: stream.position };
|
|
|
|
function readHeader() {
|
|
box.size = stream.readU32();
|
|
box.type = stream.read4CC();
|
|
}
|
|
|
|
function readFullHeader() {
|
|
box.version = stream.readU8();
|
|
box.flags = stream.readU24();
|
|
}
|
|
|
|
function remainingBytes() {
|
|
return box.size - (stream.position - box.offset);
|
|
}
|
|
|
|
function skipRemainingBytes () {
|
|
stream.skip(remainingBytes());
|
|
}
|
|
|
|
var readRemainingBoxes = function () {
|
|
var subStream = stream.subStream(stream.position, remainingBytes());
|
|
this.readBoxes(subStream, box);
|
|
stream.skip(subStream.length);
|
|
}.bind(this);
|
|
|
|
readHeader();
|
|
|
|
switch (box.type) {
|
|
case 'ftyp':
|
|
box.name = "File Type Box";
|
|
box.majorBrand = stream.read4CC();
|
|
box.minorVersion = stream.readU32();
|
|
box.compatibleBrands = new Array((box.size - 16) / 4);
|
|
for (var i = 0; i < box.compatibleBrands.length; i++) {
|
|
box.compatibleBrands[i] = stream.read4CC();
|
|
}
|
|
break;
|
|
case 'moov':
|
|
box.name = "Movie Box";
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'mvhd':
|
|
box.name = "Movie Header Box";
|
|
readFullHeader();
|
|
assert (box.version == 0);
|
|
box.creationTime = stream.readU32();
|
|
box.modificationTime = stream.readU32();
|
|
box.timeScale = stream.readU32();
|
|
box.duration = stream.readU32();
|
|
box.rate = stream.readFP16();
|
|
box.volume = stream.readFP8();
|
|
stream.skip(10);
|
|
box.matrix = stream.readU32Array(9);
|
|
stream.skip(6 * 4);
|
|
box.nextTrackId = stream.readU32();
|
|
break;
|
|
case 'trak':
|
|
box.name = "Track Box";
|
|
readRemainingBoxes();
|
|
this.tracks[box.tkhd.trackId] = new Track(this, box);
|
|
break;
|
|
case 'tkhd':
|
|
box.name = "Track Header Box";
|
|
readFullHeader();
|
|
assert (box.version == 0);
|
|
box.creationTime = stream.readU32();
|
|
box.modificationTime = stream.readU32();
|
|
box.trackId = stream.readU32();
|
|
stream.skip(4);
|
|
box.duration = stream.readU32();
|
|
stream.skip(8);
|
|
box.layer = stream.readU16();
|
|
box.alternateGroup = stream.readU16();
|
|
box.volume = stream.readFP8();
|
|
stream.skip(2);
|
|
box.matrix = stream.readU32Array(9);
|
|
box.width = stream.readFP16();
|
|
box.height = stream.readFP16();
|
|
break;
|
|
case 'mdia':
|
|
box.name = "Media Box";
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'mdhd':
|
|
box.name = "Media Header Box";
|
|
readFullHeader();
|
|
assert (box.version == 0);
|
|
box.creationTime = stream.readU32();
|
|
box.modificationTime = stream.readU32();
|
|
box.timeScale = stream.readU32();
|
|
box.duration = stream.readU32();
|
|
box.language = stream.readISO639();
|
|
stream.skip(2);
|
|
break;
|
|
case 'hdlr':
|
|
box.name = "Handler Reference Box";
|
|
readFullHeader();
|
|
stream.skip(4);
|
|
box.handlerType = stream.read4CC();
|
|
stream.skip(4 * 3);
|
|
var bytesLeft = box.size - 32;
|
|
if (bytesLeft > 0) {
|
|
box.name = stream.readUTF8(bytesLeft);
|
|
}
|
|
break;
|
|
case 'minf':
|
|
box.name = "Media Information Box";
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'stbl':
|
|
box.name = "Sample Table Box";
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'stsd':
|
|
box.name = "Sample Description Box";
|
|
readFullHeader();
|
|
box.sd = [];
|
|
var entries = stream.readU32();
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'avc1':
|
|
stream.reserved(6, 0);
|
|
box.dataReferenceIndex = stream.readU16();
|
|
assert (stream.readU16() == 0); // Version
|
|
assert (stream.readU16() == 0); // Revision Level
|
|
stream.readU32(); // Vendor
|
|
stream.readU32(); // Temporal Quality
|
|
stream.readU32(); // Spatial Quality
|
|
box.width = stream.readU16();
|
|
box.height = stream.readU16();
|
|
box.horizontalResolution = stream.readFP16();
|
|
box.verticalResolution = stream.readFP16();
|
|
assert (stream.readU32() == 0); // Reserved
|
|
box.frameCount = stream.readU16();
|
|
box.compressorName = stream.readPString(32);
|
|
box.depth = stream.readU16();
|
|
assert (stream.readU16() == 0xFFFF); // Color Table Id
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'mp4a':
|
|
stream.reserved(6, 0);
|
|
box.dataReferenceIndex = stream.readU16();
|
|
box.version = stream.readU16();
|
|
stream.skip(2);
|
|
stream.skip(4);
|
|
box.channelCount = stream.readU16();
|
|
box.sampleSize = stream.readU16();
|
|
box.compressionId = stream.readU16();
|
|
box.packetSize = stream.readU16();
|
|
box.sampleRate = stream.readU32() >>> 16;
|
|
|
|
// TODO: Parse other version levels.
|
|
assert (box.version == 0);
|
|
readRemainingBoxes();
|
|
break;
|
|
case 'esds':
|
|
box.name = "Elementary Stream Descriptor";
|
|
readFullHeader();
|
|
// TODO: Do we really need to parse this?
|
|
skipRemainingBytes();
|
|
break;
|
|
case 'avcC':
|
|
box.name = "AVC Configuration Box";
|
|
box.configurationVersion = stream.readU8();
|
|
box.avcProfileIndication = stream.readU8();
|
|
box.profileCompatibility = stream.readU8();
|
|
box.avcLevelIndication = stream.readU8();
|
|
box.lengthSizeMinusOne = stream.readU8() & 3;
|
|
assert (box.lengthSizeMinusOne == 3, "TODO");
|
|
var count = stream.readU8() & 31;
|
|
box.sps = [];
|
|
for (var i = 0; i < count; i++) {
|
|
box.sps.push(stream.readU8Array(stream.readU16()));
|
|
}
|
|
var count = stream.readU8() & 31;
|
|
box.pps = [];
|
|
for (var i = 0; i < count; i++) {
|
|
box.pps.push(stream.readU8Array(stream.readU16()));
|
|
}
|
|
skipRemainingBytes();
|
|
break;
|
|
case 'btrt':
|
|
box.name = "Bit Rate Box";
|
|
box.bufferSizeDb = stream.readU32();
|
|
box.maxBitrate = stream.readU32();
|
|
box.avgBitrate = stream.readU32();
|
|
break;
|
|
case 'stts':
|
|
box.name = "Decoding Time to Sample Box";
|
|
readFullHeader();
|
|
box.table = stream.readU32Array(stream.readU32(), 2, ["count", "delta"]);
|
|
break;
|
|
case 'stss':
|
|
box.name = "Sync Sample Box";
|
|
readFullHeader();
|
|
box.samples = stream.readU32Array(stream.readU32());
|
|
break;
|
|
case 'stsc':
|
|
box.name = "Sample to Chunk Box";
|
|
readFullHeader();
|
|
box.table = stream.readU32Array(stream.readU32(), 3,
|
|
["firstChunk", "samplesPerChunk", "sampleDescriptionId"]);
|
|
break;
|
|
case 'stsz':
|
|
box.name = "Sample Size Box";
|
|
readFullHeader();
|
|
box.sampleSize = stream.readU32();
|
|
var count = stream.readU32();
|
|
if (box.sampleSize == 0) {
|
|
box.table = stream.readU32Array(count);
|
|
}
|
|
break;
|
|
case 'stco':
|
|
box.name = "Chunk Offset Box";
|
|
readFullHeader();
|
|
box.table = stream.readU32Array(stream.readU32());
|
|
break;
|
|
case 'smhd':
|
|
box.name = "Sound Media Header Box";
|
|
readFullHeader();
|
|
box.balance = stream.readFP8();
|
|
stream.reserved(2, 0);
|
|
break;
|
|
case 'mdat':
|
|
box.name = "Media Data Box";
|
|
assert (box.size >= 8, "Cannot parse large media data yet.");
|
|
box.data = stream.readU8Array(remainingBytes());
|
|
break;
|
|
default:
|
|
skipRemainingBytes();
|
|
break;
|
|
};
|
|
return box;
|
|
},
|
|
read: function () {
|
|
var start = (new Date).getTime();
|
|
this.file = {};
|
|
this.readBoxes(this.stream, this.file);
|
|
console.info("Parsed stream in " + ((new Date).getTime() - start) + " ms");
|
|
},
|
|
traceSamples: function () {
|
|
var video = this.tracks[1];
|
|
var audio = this.tracks[2];
|
|
|
|
console.info("Video Samples: " + video.getSampleCount());
|
|
console.info("Audio Samples: " + audio.getSampleCount());
|
|
|
|
var vi = 0;
|
|
var ai = 0;
|
|
|
|
for (var i = 0; i < 100; i++) {
|
|
var vo = video.sampleToOffset(vi);
|
|
var ao = audio.sampleToOffset(ai);
|
|
|
|
var vs = video.sampleToSize(vi, 1);
|
|
var as = audio.sampleToSize(ai, 1);
|
|
|
|
if (vo < ao) {
|
|
console.info("V Sample " + vi + " Offset : " + vo + ", Size : " + vs);
|
|
vi ++;
|
|
} else {
|
|
console.info("A Sample " + ai + " Offset : " + ao + ", Size : " + as);
|
|
ai ++;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return constructor;
|
|
})();
|
|
|
|
var Track = (function track () {
|
|
function constructor(file, trak) {
|
|
this.file = file;
|
|
this.trak = trak;
|
|
}
|
|
|
|
constructor.prototype = {
|
|
getSampleSizeTable: function () {
|
|
return this.trak.mdia.minf.stbl.stsz.table;
|
|
},
|
|
getSampleCount: function () {
|
|
return this.getSampleSizeTable().length;
|
|
},
|
|
/**
|
|
* Computes the size of a range of samples, returns zero if length is zero.
|
|
*/
|
|
sampleToSize: function (start, length) {
|
|
var table = this.getSampleSizeTable();
|
|
var size = 0;
|
|
for (var i = start; i < start + length; i++) {
|
|
size += table[i];
|
|
}
|
|
return size;
|
|
},
|
|
/**
|
|
* Computes the chunk that contains the specified sample, as well as the offset of
|
|
* the sample in the computed chunk.
|
|
*/
|
|
sampleToChunk: function (sample) {
|
|
|
|
/* Samples are grouped in chunks which may contain a variable number of samples.
|
|
* The sample-to-chunk table in the stsc box describes how samples are arranged
|
|
* in chunks. Each table row corresponds to a set of consecutive chunks with the
|
|
* same number of samples and description ids. For example, the following table:
|
|
*
|
|
* +-------------+-------------------+----------------------+
|
|
* | firstChunk | samplesPerChunk | sampleDescriptionId |
|
|
* +-------------+-------------------+----------------------+
|
|
* | 1 | 3 | 23 |
|
|
* | 3 | 1 | 23 |
|
|
* | 5 | 1 | 24 |
|
|
* +-------------+-------------------+----------------------+
|
|
*
|
|
* describes 5 chunks with a total of (2 * 3) + (2 * 1) + (1 * 1) = 9 samples,
|
|
* each chunk containing samples 3, 3, 1, 1, 1 in chunk order, or
|
|
* chunks 1, 1, 1, 2, 2, 2, 3, 4, 5 in sample order.
|
|
*
|
|
* This function determines the chunk that contains a specified sample by iterating
|
|
* over every entry in the table. It also returns the position of the sample in the
|
|
* chunk which can be used to compute the sample's exact position in the file.
|
|
*
|
|
* TODO: Determine if we should memoize this function.
|
|
*/
|
|
|
|
var table = this.trak.mdia.minf.stbl.stsc.table;
|
|
|
|
if (table.length === 1) {
|
|
var row = table[0];
|
|
assert (row.firstChunk === 1);
|
|
return {
|
|
index: Math.floor(sample / row.samplesPerChunk),
|
|
offset: sample % row.samplesPerChunk
|
|
};
|
|
}
|
|
|
|
var totalChunkCount = 0;
|
|
for (var i = 0; i < table.length; i++) {
|
|
var row = table[i];
|
|
if (i > 0) {
|
|
var previousRow = table[i - 1];
|
|
var previousChunkCount = row.firstChunk - previousRow.firstChunk;
|
|
var previousSampleCount = previousRow.samplesPerChunk * previousChunkCount;
|
|
if (sample >= previousSampleCount) {
|
|
sample -= previousSampleCount;
|
|
if (i == table.length - 1) {
|
|
return {
|
|
index: totalChunkCount + previousChunkCount + Math.floor(sample / row.samplesPerChunk),
|
|
offset: sample % row.samplesPerChunk
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
index: totalChunkCount + Math.floor(sample / previousRow.samplesPerChunk),
|
|
offset: sample % previousRow.samplesPerChunk
|
|
};
|
|
}
|
|
totalChunkCount += previousChunkCount;
|
|
}
|
|
}
|
|
assert(false);
|
|
},
|
|
chunkToOffset: function (chunk) {
|
|
var table = this.trak.mdia.minf.stbl.stco.table;
|
|
return table[chunk];
|
|
},
|
|
sampleToOffset: function (sample) {
|
|
var res = this.sampleToChunk(sample);
|
|
var offset = this.chunkToOffset(res.index);
|
|
return offset + this.sampleToSize(sample - res.offset, res.offset);
|
|
},
|
|
/**
|
|
* Computes the sample at the specified time.
|
|
*/
|
|
timeToSample: function (time) {
|
|
/* In the time-to-sample table samples are grouped by their duration. The count field
|
|
* indicates the number of consecutive samples that have the same duration. For example,
|
|
* the following table:
|
|
*
|
|
* +-------+-------+
|
|
* | count | delta |
|
|
* +-------+-------+
|
|
* | 4 | 3 |
|
|
* | 2 | 1 |
|
|
* | 3 | 2 |
|
|
* +-------+-------+
|
|
*
|
|
* describes 9 samples with a total time of (4 * 3) + (2 * 1) + (3 * 2) = 20.
|
|
*
|
|
* This function determines the sample at the specified time by iterating over every
|
|
* entry in the table.
|
|
*
|
|
* TODO: Determine if we should memoize this function.
|
|
*/
|
|
var table = this.trak.mdia.minf.stbl.stts.table;
|
|
var sample = 0;
|
|
for (var i = 0; i < table.length; i++) {
|
|
var delta = table[i].count * table[i].delta;
|
|
if (time >= delta) {
|
|
time -= delta;
|
|
sample += table[i].count;
|
|
} else {
|
|
return sample + Math.floor(time / table[i].delta);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Gets the total time of the track.
|
|
*/
|
|
getTotalTime: function () {
|
|
if (PARANOID) {
|
|
var table = this.trak.mdia.minf.stbl.stts.table;
|
|
var duration = 0;
|
|
for (var i = 0; i < table.length; i++) {
|
|
duration += table[i].count * table[i].delta;
|
|
}
|
|
assert (this.trak.mdia.mdhd.duration == duration);
|
|
}
|
|
return this.trak.mdia.mdhd.duration;
|
|
},
|
|
getTotalTimeInSeconds: function () {
|
|
return this.timeToSeconds(this.getTotalTime());
|
|
},
|
|
getTimeScale: function () {
|
|
return this.trak.mdia.mdhd.timeScale;
|
|
},
|
|
/**
|
|
* Converts time units to real time (seconds).
|
|
*/
|
|
timeToSeconds: function (time) {
|
|
return time / this.getTimeScale();
|
|
},
|
|
/**
|
|
* Converts real time (seconds) to time units.
|
|
*/
|
|
secondsToTime: function (seconds) {
|
|
return seconds * this.getTimeScale();
|
|
},
|
|
foo: function () {
|
|
/*
|
|
for (var i = 0; i < this.getSampleCount(); i++) {
|
|
var res = this.sampleToChunk(i);
|
|
console.info("Sample " + i + " -> " + res.index + " % " + res.offset +
|
|
" @ " + this.chunkToOffset(res.index) +
|
|
" @@ " + this.sampleToOffset(i));
|
|
}
|
|
console.info("Total Time: " + this.timeToSeconds(this.getTotalTime()));
|
|
var total = this.getTotalTimeInSeconds();
|
|
for (var i = 50; i < total; i += 0.1) {
|
|
// console.info("Time: " + i.toFixed(2) + " " + this.secondsToTime(i));
|
|
|
|
console.info("Time: " + i.toFixed(2) + " " + this.timeToSample(this.secondsToTime(i)));
|
|
}
|
|
*/
|
|
},
|
|
/**
|
|
* AVC samples contain one or more NAL units each of which have a length prefix.
|
|
* This function returns an array of NAL units without their length prefixes.
|
|
*/
|
|
getSampleNALUnits: function (sample) {
|
|
var bytes = this.file.stream.bytes;
|
|
var offset = this.sampleToOffset(sample);
|
|
var end = offset + this.sampleToSize(sample, 1);
|
|
var nalUnits = [];
|
|
while(end - offset > 0) {
|
|
var length = (new Bytestream(bytes.buffer, offset)).readU32();
|
|
nalUnits.push(bytes.subarray(offset + 4, offset + length + 4));
|
|
offset = offset + length + 4;
|
|
}
|
|
return nalUnits;
|
|
}
|
|
};
|
|
return constructor;
|
|
})();
|
|
|
|
|
|
// Only add setZeroTimeout to the window object, and hide everything
|
|
// else in a closure. (http://dbaron.org/log/20100309-faster-timeouts)
|
|
(function() {
|
|
var timeouts = [];
|
|
var messageName = "zero-timeout-message";
|
|
|
|
// Like setTimeout, but only takes a function argument. There's
|
|
// no time argument (always zero) and no arguments (you have to
|
|
// use a closure).
|
|
function setZeroTimeout(fn) {
|
|
timeouts.push(fn);
|
|
window.postMessage(messageName, "*");
|
|
}
|
|
|
|
function handleMessage(event) {
|
|
if (event.source == window && event.data == messageName) {
|
|
event.stopPropagation();
|
|
if (timeouts.length > 0) {
|
|
var fn = timeouts.shift();
|
|
fn();
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener("message", handleMessage, true);
|
|
|
|
// Add the one thing we want added to the window object.
|
|
window.setZeroTimeout = setZeroTimeout;
|
|
})();
|
|
|
|
var MP4Player = (function reader() {
|
|
var defaultConfig = {
|
|
filter: "original",
|
|
filterHorLuma: "optimized",
|
|
filterVerLumaEdge: "optimized",
|
|
getBoundaryStrengthsA: "optimized"
|
|
};
|
|
|
|
function constructor(stream, useWorkers, webgl, render) {
|
|
this.stream = stream;
|
|
this.useWorkers = useWorkers;
|
|
this.webgl = webgl;
|
|
this.render = render;
|
|
|
|
this.statistics = {
|
|
videoStartTime: 0,
|
|
videoPictureCounter: 0,
|
|
windowStartTime: 0,
|
|
windowPictureCounter: 0,
|
|
fps: 0,
|
|
fpsMin: 1000,
|
|
fpsMax: -1000,
|
|
webGLTextureUploadTime: 0
|
|
};
|
|
|
|
this.onStatisticsUpdated = function () {};
|
|
|
|
this.avc = new Player({
|
|
useWorker: useWorkers,
|
|
reuseMemory: true,
|
|
webgl: webgl,
|
|
size: {
|
|
width: 640,
|
|
height: 368
|
|
}
|
|
});
|
|
|
|
this.webgl = this.avc.webgl;
|
|
|
|
var self = this;
|
|
this.avc.onPictureDecoded = function(){
|
|
updateStatistics.call(self);
|
|
};
|
|
|
|
this.canvas = this.avc.canvas;
|
|
}
|
|
|
|
function updateStatistics() {
|
|
var s = this.statistics;
|
|
s.videoPictureCounter += 1;
|
|
s.windowPictureCounter += 1;
|
|
var now = Date.now();
|
|
if (!s.videoStartTime) {
|
|
s.videoStartTime = now;
|
|
}
|
|
var videoElapsedTime = now - s.videoStartTime;
|
|
s.elapsed = videoElapsedTime / 1000;
|
|
if (videoElapsedTime < 1000) {
|
|
return;
|
|
}
|
|
|
|
if (!s.windowStartTime) {
|
|
s.windowStartTime = now;
|
|
return;
|
|
} else if ((now - s.windowStartTime) > 1000) {
|
|
var windowElapsedTime = now - s.windowStartTime;
|
|
var fps = (s.windowPictureCounter / windowElapsedTime) * 1000;
|
|
s.windowStartTime = now;
|
|
s.windowPictureCounter = 0;
|
|
|
|
if (fps < s.fpsMin) s.fpsMin = fps;
|
|
if (fps > s.fpsMax) s.fpsMax = fps;
|
|
s.fps = fps;
|
|
}
|
|
|
|
var fps = (s.videoPictureCounter / videoElapsedTime) * 1000;
|
|
s.fpsSinceStart = fps;
|
|
this.onStatisticsUpdated(this.statistics);
|
|
return;
|
|
}
|
|
|
|
constructor.prototype = {
|
|
readAll: function(callback) {
|
|
console.info("MP4Player::readAll()");
|
|
this.stream.readAll(null, function (buffer) {
|
|
this.reader = new MP4Reader(new Bytestream(buffer));
|
|
this.reader.read();
|
|
var video = this.reader.tracks[1];
|
|
this.size = new Size(video.trak.tkhd.width, video.trak.tkhd.height);
|
|
console.info("MP4Player::readAll(), length: " + this.reader.stream.length);
|
|
if (callback) callback();
|
|
}.bind(this));
|
|
},
|
|
play: function() {
|
|
var reader = this.reader;
|
|
|
|
if (!reader) {
|
|
this.readAll(this.play.bind(this));
|
|
return;
|
|
};
|
|
|
|
var video = reader.tracks[1];
|
|
var audio = reader.tracks[2];
|
|
|
|
var avc = reader.tracks[1].trak.mdia.minf.stbl.stsd.avc1.avcC;
|
|
var sps = avc.sps[0];
|
|
var pps = avc.pps[0];
|
|
|
|
/* Decode Sequence & Picture Parameter Sets */
|
|
this.avc.decode(sps);
|
|
this.avc.decode(pps);
|
|
|
|
/* Decode Pictures */
|
|
var pic = 0;
|
|
setTimeout(function foo() {
|
|
var avc = this.avc;
|
|
video.getSampleNALUnits(pic).forEach(function (nal) {
|
|
avc.decode(nal);
|
|
});
|
|
pic ++;
|
|
if (pic < 3000) {
|
|
setTimeout(foo.bind(this), 1);
|
|
};
|
|
}.bind(this), 1);
|
|
}
|
|
};
|
|
|
|
return constructor;
|
|
})();
|
|
|
|
var Broadway = (function broadway() {
|
|
function constructor(div) {
|
|
var src = div.attributes.src ? div.attributes.src.value : undefined;
|
|
var width = div.attributes.width ? div.attributes.width.value : 640;
|
|
var height = div.attributes.height ? div.attributes.height.value : 480;
|
|
|
|
var controls = document.createElement('div');
|
|
controls.setAttribute('style', "z-index: 100; position: absolute; bottom: 0px; background-color: rgba(0,0,0,0.8); height: 30px; width: 100%; text-align: left;");
|
|
this.info = document.createElement('div');
|
|
this.info.setAttribute('style', "font-size: 14px; font-weight: bold; padding: 6px; color: lime;");
|
|
controls.appendChild(this.info);
|
|
div.appendChild(controls);
|
|
|
|
var useWorkers = div.attributes.workers ? div.attributes.workers.value == "true" : false;
|
|
var render = div.attributes.render ? div.attributes.render.value == "true" : false;
|
|
|
|
var webgl = "auto";
|
|
if (div.attributes.webgl){
|
|
if (div.attributes.webgl.value == "true"){
|
|
webgl = true;
|
|
};
|
|
if (div.attributes.webgl.value == "false"){
|
|
webgl = false;
|
|
};
|
|
};
|
|
|
|
var infoStrPre = "Click canvas to load and play - ";
|
|
var infoStr = "";
|
|
if (useWorkers){
|
|
infoStr += "worker thread ";
|
|
}else{
|
|
infoStr += "main thread ";
|
|
};
|
|
|
|
this.player = new MP4Player(new Stream(src), useWorkers, webgl, render);
|
|
this.canvas = this.player.canvas;
|
|
this.canvas.onclick = function () {
|
|
this.play();
|
|
}.bind(this);
|
|
div.appendChild(this.canvas);
|
|
|
|
|
|
infoStr += " - webgl: " + this.player.webgl;
|
|
this.info.innerHTML = infoStrPre + infoStr;
|
|
|
|
|
|
this.score = null;
|
|
this.player.onStatisticsUpdated = function (statistics) {
|
|
if (statistics.videoPictureCounter % 10 != 0) {
|
|
return;
|
|
}
|
|
var info = "";
|
|
if (statistics.fps) {
|
|
info += " fps: " + statistics.fps.toFixed(2);
|
|
}
|
|
if (statistics.fpsSinceStart) {
|
|
info += " avg: " + statistics.fpsSinceStart.toFixed(2);
|
|
}
|
|
var scoreCutoff = 1200;
|
|
if (statistics.videoPictureCounter < scoreCutoff) {
|
|
this.score = scoreCutoff - statistics.videoPictureCounter;
|
|
} else if (statistics.videoPictureCounter == scoreCutoff) {
|
|
this.score = statistics.fpsSinceStart.toFixed(2);
|
|
}
|
|
// info += " score: " + this.score;
|
|
|
|
this.info.innerHTML = infoStr + info;
|
|
}.bind(this);
|
|
}
|
|
constructor.prototype = {
|
|
play: function () {
|
|
this.player.play();
|
|
}
|
|
};
|
|
return constructor;
|
|
})();
|
|
|
|
|
|
return {
|
|
Size,
|
|
Track,
|
|
MP4Reader,
|
|
MP4Player,
|
|
Bytestream,
|
|
Broadway,
|
|
}
|
|
|
|
})(); |