e9ae4872 by David LaPalomento

Implement mp4 inspector and tests

As preparation for building a tool to transform mp2t files into mp4s, create a javascript tool that parses an mp4 file. Remove the local qunit so that karma and direct qunit testing happens with the same version of the library. Force the tech to run during tests so that Safari doesn't use native HLS.
1 parent 9fa9fdde
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
......@@ -93,7 +93,8 @@ module.exports = function(config) {
'../tmp/expected.js',
'tsSegment-bc.js',
'../src/bin-utils.js',
'../test/*.js'
'../test/*.js',
'../test/muxer/js/mp4-inspector.js'
],
plugins: [
......
......@@ -58,7 +58,8 @@ module.exports = function(config) {
'../tmp/expected.js',
'tsSegment-bc.js',
'../src/bin-utils.js',
'../test/*.js'
'../test/*.js',
'../test/muxer/js/mp4-inspector.js'
],
plugins: [
......
(function(window, videojs) {
'use strict';
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
var
Uint8Array = window.Uint8Array,
typeBytes = function(type) {
return [
type.charCodeAt(0),
type.charCodeAt(1),
type.charCodeAt(2),
type.charCodeAt(3)
];
},
box = function(type) {
var
array = Array.prototype.slice.call(arguments, 1),
result = [],
size,
i;
// "unwrap" any arrays that were passed as arguments
// e.g. box('etc', 1, [2, 3], 4) -> box('etc', 1, 2, 3, 4)
for (i = 0; i < array.length; i++) {
if (array[i] instanceof Array) {
array.splice.apply(array, [i, 1].concat(array[i]));
}
}
size = 8 + array.length;
result[0] = (size & 0xFF000000) >> 24;
result[1] = (size & 0x00FF0000) >> 16;
result[2] = (size & 0x0000FF00) >> 8;
result[3] = size & 0xFF;
result = result.concat(typeBytes(type));
result = result.concat(array);
return result;
},
unityMatrix = [
0, 0, 0x10, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0x10, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0x40, 0, 0, 0
];
module('MP4 Inspector');
test('produces an empty array for empty input', function() {
strictEqual(videojs.inspectMp4(new Uint8Array([])).length, 0, 'returned an empty array');
});
test('can parse a Box', function() {
var box = new Uint8Array([
0x00, 0x00, 0x00, 0x00, // size 0
0x00, 0x00, 0x00, 0x00 // boxtype 0
]);
deepEqual(videojs.inspectMp4(box), [{
type: '\u0000\u0000\u0000\u0000',
size: 0,
data: box.subarray(box.byteLength)
}], 'parsed a Box');
});
test('can parse an ftyp', function() {
deepEqual(videojs.inspectMp4(new Uint8Array(box('ftyp',
0x00, 0x00, 0x00, 0x01, // major brand
0x00, 0x00, 0x00, 0x02, // minor version
0x00, 0x00, 0x00, 0x03, // compatible brands
0x00, 0x00, 0x00, 0x04 // compatible brands
))), [{
type: 'ftyp',
size: 4 * 6,
majorBrand: 1,
minorVersion: 2,
compatibleBrands: [3, 4]
}], 'parsed an ftyp');
});
test('can parse a pdin', function() {
deepEqual(videojs.inspectMp4(new Uint8Array(box('pdin',
0x01, // version 1
0x01, 0x02, 0x03, // flags
0x00, 0x00, 0x04, 0x00, // 1024 = 0x400 bytes/second rate
0x00, 0x00, 0x00, 0x01 // initial delay
))), [{
size: 20,
type: 'pdin',
version: 1,
flags: new Uint8Array([1, 2, 3]),
rate: 1024,
initialDelay: 1
}], 'parsed a pdin');
});
test('can parse an mdat', function() {
var mdat = new Uint8Array(box('mdat',
0x01, 0x02, 0x03, 0x04 // data
));
deepEqual(videojs.inspectMp4(mdat), [{
size: 12,
type: 'mdat',
data: mdat.subarray(mdat.byteLength - 4)
}], 'parsed an mdat');
});
test('can parse a free or skip', function() {
var
free = new Uint8Array(box('free',
0x01, 0x02, 0x03, 0x04)), // data
skip = new Uint8Array(box('skip',
0x01, 0x02, 0x03, 0x04)); // data
deepEqual(videojs.inspectMp4(free), [{
size: 12,
type: 'free',
data: free.subarray(free.byteLength - 4)
}], 'parsed a free');
deepEqual(videojs.inspectMp4(skip), [{
size: 12,
type: 'skip',
data: skip.subarray(skip.byteLength - 4)
}], 'parsed a skip');
});
test('can parse a moov', function() {
var data =
box('moov',
box('mvhd',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, // creation_time
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, // modification_time
0x00, 0x00, 0x00, 0x3c, // timescale
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
0x00, 0x01, 0x00, 0x00, // 1.0 rate
0x01, 0x00, // 1.0 volume
0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
unityMatrix,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // pre_defined
0x00, 0x00, 0x00, 0x02), // next_track_ID
box('trak',
box('tkhd',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, // creation_time
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, // modification_time
0x00, 0x00, 0x00, 0x01, // track_ID
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, // layer
0x00, 0x00, // alternate_group
0x00, 0x00, // non-audio track volume
0x00, 0x00, // reserved
unityMatrix,
0x00, 0x00, 0x01, 0x2c, // 300 = 0x12c width
0x00, 0x00, 0x00, 0x96), // 150 = 0x96 height
box('mdia',
box('mdhd',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, // creation_time
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, // modification_time
0x00, 0x00, 0x00, 0x3c, // timescale
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
0x15, 0xc7, // 'eng' language
0x00, 0x00),
box('hdlr',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00, // pre_defined
typeBytes('vide'), // handler_type
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0x00, 0x00, // reserved
typeBytes('one'), 0x00), // name
box('minf',
box('dinf',
box('dref',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00)), // entry_count
box('stbl',
box('stsd',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00), // entry_count
box('stts',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00), // entry_count
box('stsc',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00), // entry_count
box('stco',
0x01, // version 1
0x00, 0x00, 0x00, // flags
0x00, 0x00, 0x00, 0x00)))))); // entry_count;
deepEqual(videojs.inspectMp4(new Uint8Array(data)), [{
size: 433,
type: 'moov',
boxes: [{
type: 'mvhd',
version: 1,
flags: new Uint8Array([0, 0, 0]),
creationTime: 1,
modificationTime: 2,
timescale: 60,
duration: 600,
rate: 1,
size: 120,
volume: 1,
matrix: new Uint32Array(unityMatrix),
nextTrackId: 2
}, {
type: 'trak',
size: 305,
boxes: [{
type: 'tkhd',
flags: new Uint8Array([0, 0, 0]),
version: 1,
creationTime: 2,
modificationTime: 3,
size: 104,
trackId: 1,
duration: 600,
layer: 0,
alternateGroup: 0,
volume: 0,
matrix: new Uint32Array(unityMatrix),
width: 300,
height: 150
}, {
type: 'mdia',
size: 193,
boxes: [{
type: 'mdhd',
version: 1,
flags: new Uint8Array([0, 0, 0]),
creationTime: 2,
modificationTime: 3,
timescale: 60,
duration: 600,
language: 'eng',
size: 44
}, {
type: 'hdlr',
version: 1,
flags: new Uint8Array([0, 0, 0]),
handlerType: 'vide',
name: 'one',
size: 37
}, {
type: 'minf',
size: 104,
boxes: [{
type: 'dinf',
size: 24,
boxes: [{
type: 'dref',
dataReferences: [],
size: 16
}]}, {
type: 'stbl',
size: 72,
boxes: [{
type: 'stsd',
sampleDescriptions: [],
size: 16
}, {
type: 'stts',
timeToSamples: [],
size: 16
}, {
type: 'stsc',
sampleToChunks: [],
size: 16
}, {
type: 'stco',
chunkOffsets: [],
size: 16
}]
}]
}]
}]
}]
}], 'parsed a moov');
});
test('can parse a series of boxes', function() {
var ftyp = [
0x00, 0x00, 0x00, 0x18 // size 4 * 6 = 24
].concat(typeBytes('ftyp')).concat([
0x00, 0x00, 0x00, 0x01, // major brand
0x00, 0x00, 0x00, 0x02, // minor version
0x00, 0x00, 0x00, 0x03, // compatible brands
0x00, 0x00, 0x00, 0x04, // compatible brands
]);
deepEqual(videojs.inspectMp4(new Uint8Array(ftyp.concat(ftyp))),
[{
type: 'ftyp',
size: 4 * 6,
majorBrand: 1,
minorVersion: 2,
compatibleBrands: [3, 4]
},{
type: 'ftyp',
size: 4 * 6,
majorBrand: 1,
minorVersion: 2,
compatibleBrands: [3, 4]
}],
'parsed two boxes in series');
});
})(window, window.videojs);
(function(window, videojs) {
'use strict';
var
DataView = window.DataView,
/**
* Returns the string representation of an ASCII encoded four byte buffer.
* @param buffer {Uint8Array} a four-byte buffer to translate
* @return {string} the corresponding string
*/
parseType = function(buffer) {
var result = '';
result += String.fromCharCode(buffer[0]);
result += String.fromCharCode(buffer[1]);
result += String.fromCharCode(buffer[2]);
result += String.fromCharCode(buffer[3]);
return result;
},
// registry of handlers for individual mp4 box types
parse = {
ftyp: function(data) {
var
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
majorBrand: view.getUint32(0),
minorVersion: view.getUint32(4),
compatibleBrands: []
},
i = 8;
while (i < data.byteLength) {
result.compatibleBrands.push(view.getUint32(i));
i += 4;
}
return result;
},
dinf: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
dref: function(data) {
return {
dataReferences: []
};
},
hdlr: function(data) {
var
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
language,
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
handlerType: parseType(data.subarray(8, 12)),
name: ''
},
i = 8;
// parse out the name field
for (i = 24; i < data.byteLength; i++) {
if (data[i] === 0x00) {
// the name field is null-terminated
i++;
break;
}
result.name += String.fromCharCode(data[i]);
}
// decode UTF-8 to javascript's internal representation
// see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
result.name = window.decodeURIComponent(window.escape(result.name));
return result;
},
mdhd: function(data) {
var
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
language,
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
language: ''
};
if (result.version === 1) {
result.creationTime = view.getUint32(8); // truncating top 4 bytes
result.modificationTime = view.getUint32(16); // truncating top 4 bytes
result.timescale = view.getUint32(20);
result.duration = view.getUint32(28); // truncating top 4 bytes
}
// language is stored as an ISO-639-2/T code in an array of three 5-bit fields
// each field is the packed difference between its ASCII value and 0x60
language = view.getUint16(32);
result.language += String.fromCharCode((language >> 10) + 0x60);
result.language += String.fromCharCode(((language & 0x03c0) >> 5) + 0x60);
result.language += String.fromCharCode((language & 0x1f) + 0x60);
return result;
},
mdia: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
minf: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
moov: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
mvhd: function(data) {
var
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
// convert fixed-point, base 16 back to a number
rate: view.getUint16(32) + (view.getUint16(34) / 16),
volume: view.getUint8(36) + (view.getUint8(37) / 8),
matrix: new Uint32Array(data.subarray(48, 84)),
nextTrackId: view.getUint32(108)
};
if (result.version === 1) {
result.creationTime = view.getUint32(8); // truncating top 4 bytes
result.modificationTime = view.getUint32(16); // truncating top 4 bytes
result.timescale = view.getUint32(20);
result.duration = view.getUint32(28); // truncating top 4 bytes
}
return result;
},
pdin: function(data) {
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
return {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
rate: view.getUint32(4),
initialDelay: view.getUint32(8)
};
},
trak: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
stbl: function(data) {
return {
boxes: videojs.inspectMp4(data)
};
},
stco: function(data) {
return {
chunkOffsets: []
};
},
stsc: function(data) {
return {
sampleToChunks: []
};
},
stsd: function(data) {
return {
sampleDescriptions: []
};
},
stts: function(data) {
return {
timeToSamples: []
};
},
tkhd: function(data) {
var
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
result = {
version: view.getUint8(0),
flags: new Uint8Array(data.subarray(1, 4)),
layer: view.getUint16(44),
alternateGroup: view.getUint16(46),
// convert fixed-point, base 16 back to a number
volume: view.getUint8(48) + (view.getUint8(49) / 8),
matrix: new Uint32Array(data.subarray(52, 88)),
width: view.getUint32(88),
height: view.getUint32(92)
};
if (result.version === 1) {
result.creationTime = view.getUint32(8); // truncating top 4 bytes
result.modificationTime = view.getUint32(16); // truncating top 4 bytes
result.trackId = view.getUint32(20);
result.duration = view.getUint32(32); // truncating top 4 bytes
}
return result;
}
};
/**
* Return a javascript array of box objects parsed from an ISO base
* media file.
* @param data {Uint8Array} the binary data of the media to be inspected
* @return {array} a javascript array of potentially nested box objects
*/
videojs.inspectMp4 = function(data) {
var
i = 0,
result = [],
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
size,
type,
end,
box;
while (i < data.byteLength) {
// parse box data
size = view.getUint32(i),
type = parseType(data.subarray(i + 4, i + 8));
end = size > 1 ? i + size : data.byteLength;
// parse type-specific data
box = (parse[type] || function(data) {
return {
data: data
};
})(data.subarray(i + 8, end));
box.size = size;
box.type = type;
// store this box and move to the next
result.push(box);
i = end;
}
return result;
};
})(window, window.videojs);
......@@ -42,6 +42,9 @@
<script src="tsSegment-bc.js"></script>
<script src="../src/bin-utils.js"></script>
<!-- mp4 utilities -->
<script src="muxer/js/mp4-inspector.js"></script>
<!-- Test cases -->
<script>
module('environment');
......@@ -60,6 +63,7 @@
<script src="playlist_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
<script src="mp4-inspector_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......
......@@ -212,7 +212,9 @@ module('HLS', {
oldClearTimeout = window.clearTimeout;
oldGlobalOptions = window.videojs.getGlobalOptions();
// force the HLS tech to run
oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
videojs.Hls.supportsNativeHls = false;
oldDecrypt = videojs.Hls.Decrypter;
videojs.Hls.Decrypter = function() {};
......