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) { ...@@ -93,7 +93,8 @@ module.exports = function(config) {
93 '../tmp/expected.js', 93 '../tmp/expected.js',
94 'tsSegment-bc.js', 94 'tsSegment-bc.js',
95 '../src/bin-utils.js', 95 '../src/bin-utils.js',
96 '../test/*.js' 96 '../test/*.js',
97 '../test/muxer/js/mp4-inspector.js'
97 ], 98 ],
98 99
99 plugins: [ 100 plugins: [
......
...@@ -58,7 +58,8 @@ module.exports = function(config) { ...@@ -58,7 +58,8 @@ module.exports = function(config) {
58 '../tmp/expected.js', 58 '../tmp/expected.js',
59 'tsSegment-bc.js', 59 'tsSegment-bc.js',
60 '../src/bin-utils.js', 60 '../src/bin-utils.js',
61 '../test/*.js' 61 '../test/*.js',
62 '../test/muxer/js/mp4-inspector.js'
62 ], 63 ],
63 64
64 plugins: [ 65 plugins: [
......
1 (function(window, videojs) {
2 'use strict';
3 /*
4 ======== A Handy Little QUnit Reference ========
5 http://api.qunitjs.com/
6
7 Test methods:
8 module(name, {[setup][ ,teardown]})
9 test(name, callback)
10 expect(numberOfAssertions)
11 stop(increment)
12 start(decrement)
13 Test assertions:
14 ok(value, [message])
15 equal(actual, expected, [message])
16 notEqual(actual, expected, [message])
17 deepEqual(actual, expected, [message])
18 notDeepEqual(actual, expected, [message])
19 strictEqual(actual, expected, [message])
20 notStrictEqual(actual, expected, [message])
21 throws(block, [expected], [message])
22 */
23 var
24 Uint8Array = window.Uint8Array,
25 typeBytes = function(type) {
26 return [
27 type.charCodeAt(0),
28 type.charCodeAt(1),
29 type.charCodeAt(2),
30 type.charCodeAt(3)
31 ];
32 },
33 box = function(type) {
34 var
35 array = Array.prototype.slice.call(arguments, 1),
36 result = [],
37 size,
38 i;
39
40 // "unwrap" any arrays that were passed as arguments
41 // e.g. box('etc', 1, [2, 3], 4) -> box('etc', 1, 2, 3, 4)
42 for (i = 0; i < array.length; i++) {
43 if (array[i] instanceof Array) {
44 array.splice.apply(array, [i, 1].concat(array[i]));
45 }
46 }
47
48 size = 8 + array.length;
49
50 result[0] = (size & 0xFF000000) >> 24;
51 result[1] = (size & 0x00FF0000) >> 16;
52 result[2] = (size & 0x0000FF00) >> 8;
53 result[3] = size & 0xFF;
54 result = result.concat(typeBytes(type));
55 result = result.concat(array);
56 return result;
57 },
58 unityMatrix = [
59 0, 0, 0x10, 0,
60 0, 0, 0, 0,
61 0, 0, 0, 0,
62
63 0, 0, 0, 0,
64 0, 0, 0x10, 0,
65 0, 0, 0, 0,
66
67 0, 0, 0, 0,
68 0, 0, 0, 0,
69 0x40, 0, 0, 0
70 ];
71
72 module('MP4 Inspector');
73
74 test('produces an empty array for empty input', function() {
75 strictEqual(videojs.inspectMp4(new Uint8Array([])).length, 0, 'returned an empty array');
76 });
77
78 test('can parse a Box', function() {
79 var box = new Uint8Array([
80 0x00, 0x00, 0x00, 0x00, // size 0
81 0x00, 0x00, 0x00, 0x00 // boxtype 0
82 ]);
83 deepEqual(videojs.inspectMp4(box), [{
84 type: '\u0000\u0000\u0000\u0000',
85 size: 0,
86 data: box.subarray(box.byteLength)
87 }], 'parsed a Box');
88 });
89
90 test('can parse an ftyp', function() {
91 deepEqual(videojs.inspectMp4(new Uint8Array(box('ftyp',
92 0x00, 0x00, 0x00, 0x01, // major brand
93 0x00, 0x00, 0x00, 0x02, // minor version
94 0x00, 0x00, 0x00, 0x03, // compatible brands
95 0x00, 0x00, 0x00, 0x04 // compatible brands
96 ))), [{
97 type: 'ftyp',
98 size: 4 * 6,
99 majorBrand: 1,
100 minorVersion: 2,
101 compatibleBrands: [3, 4]
102 }], 'parsed an ftyp');
103 });
104
105 test('can parse a pdin', function() {
106 deepEqual(videojs.inspectMp4(new Uint8Array(box('pdin',
107 0x01, // version 1
108 0x01, 0x02, 0x03, // flags
109 0x00, 0x00, 0x04, 0x00, // 1024 = 0x400 bytes/second rate
110 0x00, 0x00, 0x00, 0x01 // initial delay
111 ))), [{
112 size: 20,
113 type: 'pdin',
114 version: 1,
115 flags: new Uint8Array([1, 2, 3]),
116 rate: 1024,
117 initialDelay: 1
118 }], 'parsed a pdin');
119 });
120
121 test('can parse an mdat', function() {
122 var mdat = new Uint8Array(box('mdat',
123 0x01, 0x02, 0x03, 0x04 // data
124 ));
125 deepEqual(videojs.inspectMp4(mdat), [{
126 size: 12,
127 type: 'mdat',
128 data: mdat.subarray(mdat.byteLength - 4)
129 }], 'parsed an mdat');
130 });
131
132 test('can parse a free or skip', function() {
133 var
134 free = new Uint8Array(box('free',
135 0x01, 0x02, 0x03, 0x04)), // data
136 skip = new Uint8Array(box('skip',
137 0x01, 0x02, 0x03, 0x04)); // data
138
139 deepEqual(videojs.inspectMp4(free), [{
140 size: 12,
141 type: 'free',
142 data: free.subarray(free.byteLength - 4)
143 }], 'parsed a free');
144 deepEqual(videojs.inspectMp4(skip), [{
145 size: 12,
146 type: 'skip',
147 data: skip.subarray(skip.byteLength - 4)
148 }], 'parsed a skip');
149 });
150
151 test('can parse a moov', function() {
152 var data =
153 box('moov',
154 box('mvhd',
155 0x01, // version 1
156 0x00, 0x00, 0x00, // flags
157 0x00, 0x00, 0x00, 0x00,
158 0x00, 0x00, 0x00, 0x01, // creation_time
159 0x00, 0x00, 0x00, 0x00,
160 0x00, 0x00, 0x00, 0x02, // modification_time
161 0x00, 0x00, 0x00, 0x3c, // timescale
162 0x00, 0x00, 0x00, 0x00,
163 0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
164 0x00, 0x01, 0x00, 0x00, // 1.0 rate
165 0x01, 0x00, // 1.0 volume
166 0x00, 0x00, // reserved
167 0x00, 0x00, 0x00, 0x00, // reserved
168 0x00, 0x00, 0x00, 0x00, // reserved
169 unityMatrix,
170 0x00, 0x00, 0x00, 0x00,
171 0x00, 0x00, 0x00, 0x00,
172 0x00, 0x00, 0x00, 0x00,
173 0x00, 0x00, 0x00, 0x00,
174 0x00, 0x00, 0x00, 0x00,
175 0x00, 0x00, 0x00, 0x00, // pre_defined
176 0x00, 0x00, 0x00, 0x02), // next_track_ID
177 box('trak',
178 box('tkhd',
179 0x01, // version 1
180 0x00, 0x00, 0x00, // flags
181 0x00, 0x00, 0x00, 0x00,
182 0x00, 0x00, 0x00, 0x02, // creation_time
183 0x00, 0x00, 0x00, 0x00,
184 0x00, 0x00, 0x00, 0x03, // modification_time
185 0x00, 0x00, 0x00, 0x01, // track_ID
186 0x00, 0x00, 0x00, 0x00, // reserved
187 0x00, 0x00, 0x00, 0x00,
188 0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
189 0x00, 0x00, 0x00, 0x00,
190 0x00, 0x00, 0x00, 0x00, // reserved
191 0x00, 0x00, // layer
192 0x00, 0x00, // alternate_group
193 0x00, 0x00, // non-audio track volume
194 0x00, 0x00, // reserved
195 unityMatrix,
196 0x00, 0x00, 0x01, 0x2c, // 300 = 0x12c width
197 0x00, 0x00, 0x00, 0x96), // 150 = 0x96 height
198 box('mdia',
199 box('mdhd',
200 0x01, // version 1
201 0x00, 0x00, 0x00, // flags
202 0x00, 0x00, 0x00, 0x00,
203 0x00, 0x00, 0x00, 0x02, // creation_time
204 0x00, 0x00, 0x00, 0x00,
205 0x00, 0x00, 0x00, 0x03, // modification_time
206 0x00, 0x00, 0x00, 0x3c, // timescale
207 0x00, 0x00, 0x00, 0x00,
208 0x00, 0x00, 0x02, 0x58, // 600 = 0x258 duration
209 0x15, 0xc7, // 'eng' language
210 0x00, 0x00),
211 box('hdlr',
212 0x01, // version 1
213 0x00, 0x00, 0x00, // flags
214 0x00, 0x00, 0x00, 0x00, // pre_defined
215 typeBytes('vide'), // handler_type
216 0x00, 0x00, 0x00, 0x00, // reserved
217 0x00, 0x00, 0x00, 0x00, // reserved
218 0x00, 0x00, 0x00, 0x00, // reserved
219 typeBytes('one'), 0x00), // name
220 box('minf',
221 box('dinf',
222 box('dref',
223 0x01, // version 1
224 0x00, 0x00, 0x00, // flags
225 0x00, 0x00, 0x00, 0x00)), // entry_count
226 box('stbl',
227 box('stsd',
228 0x01, // version 1
229 0x00, 0x00, 0x00, // flags
230 0x00, 0x00, 0x00, 0x00), // entry_count
231 box('stts',
232 0x01, // version 1
233 0x00, 0x00, 0x00, // flags
234 0x00, 0x00, 0x00, 0x00), // entry_count
235 box('stsc',
236 0x01, // version 1
237 0x00, 0x00, 0x00, // flags
238 0x00, 0x00, 0x00, 0x00), // entry_count
239 box('stco',
240 0x01, // version 1
241 0x00, 0x00, 0x00, // flags
242 0x00, 0x00, 0x00, 0x00)))))); // entry_count;
243
244 deepEqual(videojs.inspectMp4(new Uint8Array(data)), [{
245 size: 433,
246 type: 'moov',
247 boxes: [{
248 type: 'mvhd',
249 version: 1,
250 flags: new Uint8Array([0, 0, 0]),
251 creationTime: 1,
252 modificationTime: 2,
253 timescale: 60,
254 duration: 600,
255 rate: 1,
256 size: 120,
257 volume: 1,
258 matrix: new Uint32Array(unityMatrix),
259 nextTrackId: 2
260 }, {
261 type: 'trak',
262 size: 305,
263 boxes: [{
264 type: 'tkhd',
265 flags: new Uint8Array([0, 0, 0]),
266 version: 1,
267 creationTime: 2,
268 modificationTime: 3,
269 size: 104,
270 trackId: 1,
271 duration: 600,
272 layer: 0,
273 alternateGroup: 0,
274 volume: 0,
275 matrix: new Uint32Array(unityMatrix),
276 width: 300,
277 height: 150
278 }, {
279 type: 'mdia',
280 size: 193,
281 boxes: [{
282 type: 'mdhd',
283 version: 1,
284 flags: new Uint8Array([0, 0, 0]),
285 creationTime: 2,
286 modificationTime: 3,
287 timescale: 60,
288 duration: 600,
289 language: 'eng',
290 size: 44
291 }, {
292 type: 'hdlr',
293 version: 1,
294 flags: new Uint8Array([0, 0, 0]),
295 handlerType: 'vide',
296 name: 'one',
297 size: 37
298 }, {
299 type: 'minf',
300 size: 104,
301 boxes: [{
302 type: 'dinf',
303 size: 24,
304 boxes: [{
305 type: 'dref',
306 dataReferences: [],
307 size: 16
308 }]}, {
309 type: 'stbl',
310 size: 72,
311 boxes: [{
312 type: 'stsd',
313 sampleDescriptions: [],
314 size: 16
315 }, {
316 type: 'stts',
317 timeToSamples: [],
318 size: 16
319 }, {
320 type: 'stsc',
321 sampleToChunks: [],
322 size: 16
323 }, {
324 type: 'stco',
325 chunkOffsets: [],
326 size: 16
327 }]
328 }]
329 }]
330 }]
331 }]
332 }], 'parsed a moov');
333 });
334
335 test('can parse a series of boxes', function() {
336 var ftyp = [
337 0x00, 0x00, 0x00, 0x18 // size 4 * 6 = 24
338 ].concat(typeBytes('ftyp')).concat([
339 0x00, 0x00, 0x00, 0x01, // major brand
340 0x00, 0x00, 0x00, 0x02, // minor version
341 0x00, 0x00, 0x00, 0x03, // compatible brands
342 0x00, 0x00, 0x00, 0x04, // compatible brands
343 ]);
344
345 deepEqual(videojs.inspectMp4(new Uint8Array(ftyp.concat(ftyp))),
346 [{
347 type: 'ftyp',
348 size: 4 * 6,
349 majorBrand: 1,
350 minorVersion: 2,
351 compatibleBrands: [3, 4]
352 },{
353 type: 'ftyp',
354 size: 4 * 6,
355 majorBrand: 1,
356 minorVersion: 2,
357 compatibleBrands: [3, 4]
358 }],
359 'parsed two boxes in series');
360
361 });
362
363 })(window, window.videojs);
1 (function(window, videojs) {
2 'use strict';
3
4 var
5 DataView = window.DataView,
6 /**
7 * Returns the string representation of an ASCII encoded four byte buffer.
8 * @param buffer {Uint8Array} a four-byte buffer to translate
9 * @return {string} the corresponding string
10 */
11 parseType = function(buffer) {
12 var result = '';
13 result += String.fromCharCode(buffer[0]);
14 result += String.fromCharCode(buffer[1]);
15 result += String.fromCharCode(buffer[2]);
16 result += String.fromCharCode(buffer[3]);
17 return result;
18 },
19
20 // registry of handlers for individual mp4 box types
21 parse = {
22 ftyp: function(data) {
23 var
24 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
25 result = {
26 majorBrand: view.getUint32(0),
27 minorVersion: view.getUint32(4),
28 compatibleBrands: []
29 },
30 i = 8;
31 while (i < data.byteLength) {
32 result.compatibleBrands.push(view.getUint32(i));
33 i += 4;
34 }
35 return result;
36 },
37 dinf: function(data) {
38 return {
39 boxes: videojs.inspectMp4(data)
40 };
41 },
42 dref: function(data) {
43 return {
44 dataReferences: []
45 };
46 },
47 hdlr: function(data) {
48 var
49 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
50 language,
51 result = {
52 version: view.getUint8(0),
53 flags: new Uint8Array(data.subarray(1, 4)),
54 handlerType: parseType(data.subarray(8, 12)),
55 name: ''
56 },
57 i = 8;
58
59 // parse out the name field
60 for (i = 24; i < data.byteLength; i++) {
61 if (data[i] === 0x00) {
62 // the name field is null-terminated
63 i++;
64 break;
65 }
66 result.name += String.fromCharCode(data[i]);
67 }
68 // decode UTF-8 to javascript's internal representation
69 // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
70 result.name = window.decodeURIComponent(window.escape(result.name));
71
72 return result;
73 },
74 mdhd: function(data) {
75 var
76 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
77 language,
78 result = {
79 version: view.getUint8(0),
80 flags: new Uint8Array(data.subarray(1, 4)),
81 language: ''
82 };
83 if (result.version === 1) {
84 result.creationTime = view.getUint32(8); // truncating top 4 bytes
85 result.modificationTime = view.getUint32(16); // truncating top 4 bytes
86 result.timescale = view.getUint32(20);
87 result.duration = view.getUint32(28); // truncating top 4 bytes
88 }
89 // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
90 // each field is the packed difference between its ASCII value and 0x60
91 language = view.getUint16(32);
92 result.language += String.fromCharCode((language >> 10) + 0x60);
93 result.language += String.fromCharCode(((language & 0x03c0) >> 5) + 0x60);
94 result.language += String.fromCharCode((language & 0x1f) + 0x60);
95
96 return result;
97 },
98 mdia: function(data) {
99 return {
100 boxes: videojs.inspectMp4(data)
101 };
102 },
103 minf: function(data) {
104 return {
105 boxes: videojs.inspectMp4(data)
106 };
107 },
108 moov: function(data) {
109 return {
110 boxes: videojs.inspectMp4(data)
111 };
112 },
113 mvhd: function(data) {
114 var
115 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
116 result = {
117 version: view.getUint8(0),
118 flags: new Uint8Array(data.subarray(1, 4)),
119 // convert fixed-point, base 16 back to a number
120 rate: view.getUint16(32) + (view.getUint16(34) / 16),
121 volume: view.getUint8(36) + (view.getUint8(37) / 8),
122 matrix: new Uint32Array(data.subarray(48, 84)),
123 nextTrackId: view.getUint32(108)
124 };
125 if (result.version === 1) {
126 result.creationTime = view.getUint32(8); // truncating top 4 bytes
127 result.modificationTime = view.getUint32(16); // truncating top 4 bytes
128 result.timescale = view.getUint32(20);
129 result.duration = view.getUint32(28); // truncating top 4 bytes
130 }
131 return result;
132 },
133 pdin: function(data) {
134 var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
135 return {
136 version: view.getUint8(0),
137 flags: new Uint8Array(data.subarray(1, 4)),
138 rate: view.getUint32(4),
139 initialDelay: view.getUint32(8)
140 };
141 },
142 trak: function(data) {
143 return {
144 boxes: videojs.inspectMp4(data)
145 };
146 },
147 stbl: function(data) {
148 return {
149 boxes: videojs.inspectMp4(data)
150 };
151 },
152 stco: function(data) {
153 return {
154 chunkOffsets: []
155 };
156 },
157 stsc: function(data) {
158 return {
159 sampleToChunks: []
160 };
161 },
162 stsd: function(data) {
163 return {
164 sampleDescriptions: []
165 };
166 },
167 stts: function(data) {
168 return {
169 timeToSamples: []
170 };
171 },
172 tkhd: function(data) {
173 var
174 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
175 result = {
176 version: view.getUint8(0),
177 flags: new Uint8Array(data.subarray(1, 4)),
178 layer: view.getUint16(44),
179 alternateGroup: view.getUint16(46),
180 // convert fixed-point, base 16 back to a number
181 volume: view.getUint8(48) + (view.getUint8(49) / 8),
182 matrix: new Uint32Array(data.subarray(52, 88)),
183 width: view.getUint32(88),
184 height: view.getUint32(92)
185 };
186 if (result.version === 1) {
187 result.creationTime = view.getUint32(8); // truncating top 4 bytes
188 result.modificationTime = view.getUint32(16); // truncating top 4 bytes
189 result.trackId = view.getUint32(20);
190 result.duration = view.getUint32(32); // truncating top 4 bytes
191 }
192 return result;
193 }
194 };
195
196 /**
197 * Return a javascript array of box objects parsed from an ISO base
198 * media file.
199 * @param data {Uint8Array} the binary data of the media to be inspected
200 * @return {array} a javascript array of potentially nested box objects
201 */
202 videojs.inspectMp4 = function(data) {
203 var
204 i = 0,
205 result = [],
206 view = new DataView(data.buffer, data.byteOffset, data.byteLength),
207 size,
208 type,
209 end,
210 box;
211
212 while (i < data.byteLength) {
213 // parse box data
214 size = view.getUint32(i),
215 type = parseType(data.subarray(i + 4, i + 8));
216 end = size > 1 ? i + size : data.byteLength;
217
218 // parse type-specific data
219 box = (parse[type] || function(data) {
220 return {
221 data: data
222 };
223 })(data.subarray(i + 8, end));
224 box.size = size;
225 box.type = type;
226
227 // store this box and move to the next
228 result.push(box);
229 i = end;
230 }
231 return result;
232 };
233 })(window, window.videojs);
...@@ -42,6 +42,9 @@ ...@@ -42,6 +42,9 @@
42 <script src="tsSegment-bc.js"></script> 42 <script src="tsSegment-bc.js"></script>
43 <script src="../src/bin-utils.js"></script> 43 <script src="../src/bin-utils.js"></script>
44 44
45 <!-- mp4 utilities -->
46 <script src="muxer/js/mp4-inspector.js"></script>
47
45 <!-- Test cases --> 48 <!-- Test cases -->
46 <script> 49 <script>
47 module('environment'); 50 module('environment');
...@@ -60,6 +63,7 @@ ...@@ -60,6 +63,7 @@
60 <script src="playlist_test.js"></script> 63 <script src="playlist_test.js"></script>
61 <script src="playlist-loader_test.js"></script> 64 <script src="playlist-loader_test.js"></script>
62 <script src="decrypter_test.js"></script> 65 <script src="decrypter_test.js"></script>
66 <script src="mp4-inspector_test.js"></script>
63 </head> 67 </head>
64 <body> 68 <body>
65 <div id="qunit"></div> 69 <div id="qunit"></div>
......
...@@ -212,7 +212,9 @@ module('HLS', { ...@@ -212,7 +212,9 @@ module('HLS', {
212 oldClearTimeout = window.clearTimeout; 212 oldClearTimeout = window.clearTimeout;
213 oldGlobalOptions = window.videojs.getGlobalOptions(); 213 oldGlobalOptions = window.videojs.getGlobalOptions();
214 214
215 // force the HLS tech to run
215 oldNativeHlsSupport = videojs.Hls.supportsNativeHls; 216 oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
217 videojs.Hls.supportsNativeHls = false;
216 218
217 oldDecrypt = videojs.Hls.Decrypter; 219 oldDecrypt = videojs.Hls.Decrypter;
218 videojs.Hls.Decrypter = function() {}; 220 videojs.Hls.Decrypter = function() {};
......