5cfc5611 by David LaPalomento

Generate init segment based on elementary streams

Capture the number of elementary streams in a transport stream program and map those to mp4 tracks in the init segment. Fix up some examples and update MSE documentation.
1 parent c080e98d
...@@ -9,8 +9,18 @@ Chrome 36. ...@@ -9,8 +9,18 @@ Chrome 36.
9 ## ISO Base Media File Format (BMFF) 9 ## ISO Base Media File Format (BMFF)
10 10
11 ### Init Segment 11 ### Init Segment
12 A working initialization segment is outlined below. It may be possible
13 to trim this structure down further.
14
12 - `ftyp` 15 - `ftyp`
13 - `moov` 16 - `moov`
17 - `mvhd`
18 - `trak`
19 - `tkhd`
20 - `mdia`
21 - `mdhd`
22 - `hdlr`
23 - `minf`
14 - `mvex` 24 - `mvex`
15 25
16 ### Media Segment 26 ### Media Segment
...@@ -22,7 +32,7 @@ movie data is outlined below: ...@@ -22,7 +32,7 @@ movie data is outlined below:
22 - `traf` 32 - `traf`
23 - `tfhd` 33 - `tfhd`
24 - `tfdt` 34 - `tfdt`
25 - `trun` 35 - `trun` containing samples
26 - `mdat` 36 - `mdat`
27 37
28 ### Structure 38 ### Structure
......
...@@ -221,8 +221,19 @@ moof = function(sequenceNumber, tracks) { ...@@ -221,8 +221,19 @@ moof = function(sequenceNumber, tracks) {
221 mfhd(sequenceNumber), 221 mfhd(sequenceNumber),
222 box.apply(null, trafCall)); 222 box.apply(null, trafCall));
223 }; 223 };
224 moov = function(duration, width, height, type) { 224 /**
225 return box(types.moov, mvhd(duration), trak(duration, width, height, type), mvex()); 225 * @param tracks... (optional) {array} the tracks associated with this movie
226 */
227 moov = function(tracks) {
228 var
229 i = tracks.length,
230 boxes = [];
231
232 while (i--) {
233 boxes[i] = trak(tracks[i]);
234 }
235
236 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex()));
226 }; 237 };
227 mvex = function() { 238 mvex = function() {
228 return box(types.mvex, box(types.trex, TREX)); 239 return box(types.mvex, box(types.trex, TREX));
...@@ -331,18 +342,21 @@ styp = function() { ...@@ -331,18 +342,21 @@ styp = function() {
331 return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); 342 return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND);
332 }; 343 };
333 344
334 tkhd = function(duration, width, height) { 345 tkhd = function(track) {
335 return box(types.tkhd, new Uint8Array([ 346 return box(types.tkhd, new Uint8Array([
336 0x00, // version 0 347 0x00, // version 0
337 0x00, 0x00, 0x00, // flags 348 0x00, 0x00, 0x00, // flags
338 0x00, 0x00, 0x00, 0x00, // creation_time 349 0x00, 0x00, 0x00, 0x00, // creation_time
339 0x00, 0x00, 0x00, 0x00, // modification_time 350 0x00, 0x00, 0x00, 0x00, // modification_time
340 0x00, 0x00, 0x00, 0x01, // track_ID 351 (track.id & 0xFF000000) >> 24,
352 (track.id & 0xFF0000) >> 16,
353 (track.id & 0xFF00) >> 8,
354 track.id & 0xFF, // track_ID
341 0x00, 0x00, 0x00, 0x00, // reserved 355 0x00, 0x00, 0x00, 0x00, // reserved
342 (duration & 0xFF000000) >> 24, 356 (track.duration & 0xFF000000) >> 24,
343 (duration & 0xFF0000) >> 16, 357 (track.duration & 0xFF0000) >> 16,
344 (duration & 0xFF00) >> 8, 358 (track.duration & 0xFF00) >> 8,
345 duration & 0xFF, // duration 359 track.duration & 0xFF, // duration
346 0x00, 0x00, 0x00, 0x00, 360 0x00, 0x00, 0x00, 0x00,
347 0x00, 0x00, 0x00, 0x00, // reserved 361 0x00, 0x00, 0x00, 0x00, // reserved
348 0x00, 0x00, // layer 362 0x00, 0x00, // layer
...@@ -358,17 +372,25 @@ tkhd = function(duration, width, height) { ...@@ -358,17 +372,25 @@ tkhd = function(duration, width, height) {
358 0x00, 0x00, 0x00, 0x00, 372 0x00, 0x00, 0x00, 0x00,
359 0x00, 0x00, 0x00, 0x00, 373 0x00, 0x00, 0x00, 0x00,
360 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix 374 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
361 (width & 0xFF00) >> 8, 375 (track.width & 0xFF00) >> 8,
362 width & 0xFF, 376 track.width & 0xFF,
363 0x00, 0x00, // width 377 0x00, 0x00, // width
364 (height & 0xFF00) >> 8, 378 (track.height & 0xFF00) >> 8,
365 height & 0xFF, 379 track.height & 0xFF,
366 0x00, 0x00 // height 380 0x00, 0x00 // height
367 ])); 381 ]));
368 }; 382 };
369 383
370 trak = function(duration, width, height, type) { 384 /**
371 return box(types.trak, tkhd(duration, width, height), mdia(duration, width, height, type)); 385 * Generate a track box.
386 * @param track {object} a track definition
387 * @return {Uint8Array} the track box
388 */
389 trak = function(track) {
390 track.duration = track.duration || 0xffffffff;
391 return box(types.trak,
392 tkhd(track),
393 mdia(track.duration, track.width, track.height, track.type));
372 }; 394 };
373 395
374 window.videojs.mp4 = { 396 window.videojs.mp4 = {
...@@ -376,12 +398,13 @@ window.videojs.mp4 = { ...@@ -376,12 +398,13 @@ window.videojs.mp4 = {
376 mdat: mdat, 398 mdat: mdat,
377 moof: moof, 399 moof: moof,
378 moov: moov, 400 moov: moov,
379 initSegment: function() { 401 initSegment: function(tracks) {
380 var 402 var
381 fileType = ftyp(), 403 fileType = ftyp(),
382 movie = moov(0xffffffff, 1280, 720, "video"), 404 movie = moov(tracks),
383 result = new Uint8Array(fileType.byteLength + movie.byteLength); 405 result;
384 406
407 result = new Uint8Array(fileType.byteLength + movie.byteLength);
385 result.set(fileType); 408 result.set(fileType);
386 result.set(movie, fileType.byteLength); 409 result.set(movie, fileType.byteLength);
387 return result; 410 return result;
......
...@@ -124,10 +124,7 @@ ParseStream = function() { ...@@ -124,10 +124,7 @@ ParseStream = function() {
124 * fields parsed from the PMT. 124 * fields parsed from the PMT.
125 */ 125 */
126 parsePmt = function(payload, pmt) { 126 parsePmt = function(payload, pmt) {
127 var tableEnd, programInfoLength, offset; 127 var sectionLength, tableEnd, programInfoLength, offset;
128
129 pmt.section_number = payload[6];
130 pmt.last_section_number = payload[7];
131 128
132 // PMTs can be sent ahead of the time when they should actually 129 // PMTs can be sent ahead of the time when they should actually
133 // take effect. We don't believe this should ever be the case 130 // take effect. We don't believe this should ever be the case
...@@ -141,9 +138,11 @@ ParseStream = function() { ...@@ -141,9 +138,11 @@ ParseStream = function() {
141 // overwrite any existing program map table 138 // overwrite any existing program map table
142 self.programMapTable = {}; 139 self.programMapTable = {};
143 140
144 // the mapping table ends right before the 32-bit CRC 141 // the mapping table ends at the end of the current section
145 tableEnd = payload.byteLength - 4; 142 sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
146 // to determine where the table starts, we have to figure out how 143 tableEnd = 3 + sectionLength - 4;
144
145 // to determine where the table is, we have to figure out how
147 // long the program info descriptors are 146 // long the program info descriptors are
148 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; 147 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11];
149 148
...@@ -273,7 +272,7 @@ ProgramStream = function() { ...@@ -273,7 +272,7 @@ ProgramStream = function() {
273 data: [], 272 data: [],
274 size: 0 273 size: 0
275 }, 274 },
276 flushStream = function(stream, type, pes) { 275 flushStream = function(stream, type) {
277 var 276 var
278 event = { 277 event = {
279 type: type, 278 type: type,
...@@ -282,16 +281,6 @@ ProgramStream = function() { ...@@ -282,16 +281,6 @@ ProgramStream = function() {
282 i = 0, 281 i = 0,
283 fragment; 282 fragment;
284 283
285 if ( pes !== undefined) {
286 // move over data from PES into Stream frame
287 event.pes = {};
288 event.pes.pts = pes.pts;
289 event.pes.dts = pes.dts;
290 event.pes.pid = pes.pid;
291 event.pes.dataAlignmentIndicator = pes.dataAlignmentIndicator;
292 event.pes.payloadUnitStartIndicator = pes.payloadUnitStartIndicator;
293 }
294
295 // do nothing if there is no buffered data 284 // do nothing if there is no buffered data
296 if (!stream.data.length) { 285 if (!stream.data.length) {
297 return; 286 return;
...@@ -333,7 +322,7 @@ ProgramStream = function() { ...@@ -333,7 +322,7 @@ ProgramStream = function() {
333 // if a new packet is starting, we can flush the completed 322 // if a new packet is starting, we can flush the completed
334 // packet 323 // packet
335 if (data.payloadUnitStartIndicator) { 324 if (data.payloadUnitStartIndicator) {
336 flushStream(stream, streamType, data); 325 flushStream(stream, streamType);
337 } 326 }
338 327
339 // buffer this fragment until we are sure we've received the 328 // buffer this fragment until we are sure we've received the
...@@ -358,8 +347,10 @@ ProgramStream = function() { ...@@ -358,8 +347,10 @@ ProgramStream = function() {
358 track.id = +k; 347 track.id = +k;
359 if (programMapTable[k] === H264_STREAM_TYPE) { 348 if (programMapTable[k] === H264_STREAM_TYPE) {
360 track.codec = 'avc'; 349 track.codec = 'avc';
350 track.type = 'video';
361 } else if (programMapTable[k] === ADTS_STREAM_TYPE) { 351 } else if (programMapTable[k] === ADTS_STREAM_TYPE) {
362 track.codec = 'adts'; 352 track.codec = 'adts';
353 track.type = 'audio';
363 } 354 }
364 event.tracks.push(track); 355 event.tracks.push(track);
365 } 356 }
...@@ -412,9 +403,18 @@ H264Stream = function() { ...@@ -412,9 +403,18 @@ H264Stream = function() {
412 self = this; 403 self = this;
413 404
414 this.push = function(packet) { 405 this.push = function(packet) {
415 if (packet.type === 'video') { 406 if (packet.type !== 'video') {
416 this.trigger('data', packet); 407 return;
408 }
409 switch (packet.data[0]) {
410 case 0x09:
411 packet.nalUnitType = 'access_unit_delimiter_rbsp';
412 break;
413
414 default:
415 break;
417 } 416 }
417 this.trigger('data', packet);
418 }; 418 };
419 }; 419 };
420 H264Stream.prototype = new videojs.Hls.Stream(); 420 H264Stream.prototype = new videojs.Hls.Stream();
...@@ -424,7 +424,14 @@ Transmuxer = function() { ...@@ -424,7 +424,14 @@ Transmuxer = function() {
424 var 424 var
425 self = this, 425 self = this,
426 sequenceNumber = 0, 426 sequenceNumber = 0,
427 packetStream, parseStream, programStream, aacStream, h264Stream; 427 initialized = false,
428 videoSamples = [],
429 videoSamplesSize = 0,
430
431 packetStream, parseStream, programStream, aacStream, h264Stream,
432
433 flushVideo;
434
428 Transmuxer.prototype.init.call(this); 435 Transmuxer.prototype.init.call(this);
429 436
430 // set up the parsing pipeline 437 // set up the parsing pipeline
...@@ -439,16 +446,26 @@ Transmuxer = function() { ...@@ -439,16 +446,26 @@ Transmuxer = function() {
439 programStream.pipe(aacStream); 446 programStream.pipe(aacStream);
440 programStream.pipe(h264Stream); 447 programStream.pipe(h264Stream);
441 448
442 // generate an init segment 449 // helper functions
443 this.initSegment = mp4.initSegment(); 450 flushVideo = function() {
451 var moof, mdat, boxes, i, data;
444 452
445 h264Stream.on('data', function(data) { 453 moof = mp4.moof(sequenceNumber, []);
446 var 454
447 moof = mp4.moof(sequenceNumber, []), 455 // concatenate the video data and construct the mdat
448 mdat = mp4.mdat(data.data), 456 data = new Uint8Array(videoSamplesSize);
449 // it would be great to allocate this array up front instead of 457 i = 0;
450 // throwing away hundreds of media segment fragments 458 while (videoSamples.length) {
451 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 459 data.set(videoSamples[0].data, i);
460 i += videoSamples[0].data.byteLength;
461 videoSamples.shift();
462 }
463 videoSamplesSize = 0;
464 mdat = mp4.mdat(data);
465
466 // it would be great to allocate this array up front instead of
467 // throwing away hundreds of media segment fragments
468 boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
452 469
453 // bump the sequence number for next time 470 // bump the sequence number for next time
454 sequenceNumber++; 471 sequenceNumber++;
...@@ -459,13 +476,41 @@ Transmuxer = function() { ...@@ -459,13 +476,41 @@ Transmuxer = function() {
459 self.trigger('data', { 476 self.trigger('data', {
460 data: boxes 477 data: boxes
461 }); 478 });
479 };
480
481 // handle incoming data events
482 h264Stream.on('data', function(data) {
483 // if this chunk starts a new access unit, flush the data we've been buffering
484 if (data.nalUnitType === 'access_unit_delimiter_rbsp' &&
485 videoSamples.length) {
486 flushVideo();
487 }
488
489 // buffer video until we encounter a new access unit (aka the next frame)
490 videoSamples.push(data);
491 videoSamplesSize += data.data.byteLength;
492 });
493 programStream.on('data', function(data) {
494 // generate init segments based on stream metadata
495 if (!initialized && data.type === 'metadata') {
496 self.trigger('data', {
497 data: mp4.initSegment(data.tracks)
498 });
499 initialized = true;
500 }
462 }); 501 });
502
463 // feed incoming data to the front of the parsing pipeline 503 // feed incoming data to the front of the parsing pipeline
464 this.push = function(data) { 504 this.push = function(data) {
465 packetStream.push(data); 505 packetStream.push(data);
466 }; 506 };
467 // flush any buffered data 507 // flush any buffered data
468 this.end = programStream.end; 508 this.end = function() {
509 programStream.end();
510 if (videoSamples.length) {
511 flushVideo();
512 }
513 };
469 }; 514 };
470 Transmuxer.prototype = new videojs.Hls.Stream(); 515 Transmuxer.prototype = new videojs.Hls.Stream();
471 516
......
...@@ -41,7 +41,13 @@ test('generates a BSMFF ftyp', function() { ...@@ -41,7 +41,13 @@ test('generates a BSMFF ftyp', function() {
41 41
42 test('generates a moov', function() { 42 test('generates a moov', function() {
43 var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex, 43 var boxes, mvhd, tkhd, mdhd, hdlr, minf, mvex,
44 data = mp4.moov(100, 600, 300, "video"); 44 data = mp4.moov([{
45 id: 7,
46 duration: 100,
47 width: 600,
48 height: 300,
49 type: 'video'
50 }]);
45 51
46 ok(data, 'box is not null'); 52 ok(data, 'box is not null');
47 53
...@@ -53,12 +59,13 @@ test('generates a moov', function() { ...@@ -53,12 +59,13 @@ test('generates a moov', function() {
53 59
54 mvhd = boxes[0].boxes[0]; 60 mvhd = boxes[0].boxes[0];
55 equal(mvhd.type, 'mvhd', 'generated a mvhd'); 61 equal(mvhd.type, 'mvhd', 'generated a mvhd');
56 equal(mvhd.duration, 100, 'wrote the movie header duration'); 62 equal(mvhd.duration, 0xffffffff, 'wrote the maximum movie header duration');
57 63
58 equal(boxes[0].boxes[1].type, 'trak', 'generated a trak'); 64 equal(boxes[0].boxes[1].type, 'trak', 'generated a trak');
59 equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes'); 65 equal(boxes[0].boxes[1].boxes.length, 2, 'generated two track sub boxes');
60 tkhd = boxes[0].boxes[1].boxes[0]; 66 tkhd = boxes[0].boxes[1].boxes[0];
61 equal(tkhd.type, 'tkhd', 'generated a tkhd'); 67 equal(tkhd.type, 'tkhd', 'generated a tkhd');
68 equal(tkhd.trackId, 7, 'wrote the track id');
62 equal(tkhd.duration, 100, 'wrote duration into the track header'); 69 equal(tkhd.duration, 100, 'wrote duration into the track header');
63 equal(tkhd.width, 600, 'wrote width into the track header'); 70 equal(tkhd.width, 600, 'wrote width into the track header');
64 equal(tkhd.height, 300, 'wrote height into the track header'); 71 equal(tkhd.height, 300, 'wrote height into the track header');
...@@ -69,7 +76,7 @@ test('generates a moov', function() { ...@@ -69,7 +76,7 @@ test('generates a moov', function() {
69 mdhd = boxes[0].boxes[1].boxes[1].boxes[0]; 76 mdhd = boxes[0].boxes[1].boxes[1].boxes[0];
70 equal(mdhd.type, 'mdhd', 'generate an mdhd type'); 77 equal(mdhd.type, 'mdhd', 'generate an mdhd type');
71 equal(mdhd.language, 'und', 'wrote undetermined language'); 78 equal(mdhd.language, 'und', 'wrote undetermined language');
72 equal(mdhd.duration, 100, 'wrote duraiton into the media header'); 79 equal(mdhd.duration, 100, 'wrote duration into the media header');
73 80
74 hdlr = boxes[0].boxes[1].boxes[1].boxes[1]; 81 hdlr = boxes[0].boxes[1].boxes[1].boxes[1];
75 equal(hdlr.type, 'hdlr', 'generate an hdlr type'); 82 equal(hdlr.type, 'hdlr', 'generate an hdlr type');
...@@ -206,7 +213,12 @@ test('generates a moov', function() { ...@@ -206,7 +213,12 @@ test('generates a moov', function() {
206 213
207 test('generates a sound hdlr', function() { 214 test('generates a sound hdlr', function() {
208 var boxes, hdlr, 215 var boxes, hdlr,
209 data = mp4.moov(100, 600, 300, "audio"); 216 data = mp4.moov([{
217 duration:100,
218 width: 600,
219 height: 300,
220 type: 'audio'
221 }]);
210 222
211 ok(data, 'box is not null'); 223 ok(data, 'box is not null');
212 224
...@@ -220,7 +232,12 @@ test('generates a sound hdlr', function() { ...@@ -220,7 +232,12 @@ test('generates a sound hdlr', function() {
220 232
221 test('generates a video hdlr', function() { 233 test('generates a video hdlr', function() {
222 var boxes, hdlr, 234 var boxes, hdlr,
223 data = mp4.moov(100, 600, 300, "video"); 235 data = mp4.moov([{
236 duration: 100,
237 width: 600,
238 height: 300,
239 type: 'video'
240 }]);
224 241
225 ok(data, 'box is not null'); 242 ok(data, 'box is not null');
226 243
...@@ -234,14 +251,40 @@ test('generates a video hdlr', function() { ...@@ -234,14 +251,40 @@ test('generates a video hdlr', function() {
234 251
235 test('generates an initialization segment', function() { 252 test('generates an initialization segment', function() {
236 var 253 var
237 data = mp4.initSegment(), 254 data = mp4.initSegment([{
238 init; 255 id: 1,
256 width: 600,
257 height: 300,
258 type: 'video'
259 }, {
260 id: 2,
261 type: 'audio'
262 }]),
263 init, mvhd, trak1, trak2, mvex;
239 264
240 init = videojs.inspectMp4(data); 265 init = videojs.inspectMp4(data);
241 equal(init.length, 2, 'generated two boxes'); 266 equal(init.length, 2, 'generated two boxes');
242 equal(init[0].type, 'ftyp', 'generated a ftyp box'); 267 equal(init[0].type, 'ftyp', 'generated a ftyp box');
243 equal(init[1].type, 'moov', 'generated a moov box'); 268 equal(init[1].type, 'moov', 'generated a moov box');
244 equal(init[1].boxes[0].duration, 0xffffffff, 'wrote a maximum duration'); 269 equal(init[1].boxes[0].duration, 0xffffffff, 'wrote a maximum duration');
270
271 mvhd = init[1].boxes[0];
272 equal(mvhd.type, 'mvhd', 'wrote an mvhd');
273
274 trak1 = init[1].boxes[1];
275 equal(trak1.type, 'trak', 'wrote a trak');
276 equal(trak1.boxes[0].trackId, 1, 'wrote the first track id');
277 equal(trak1.boxes[0].width, 600, 'wrote the first track width');
278 equal(trak1.boxes[0].height, 300, 'wrote the first track height');
279 equal(trak1.boxes[1].boxes[1].handlerType, 'vide', 'wrote the first track type');
280
281 trak2 = init[1].boxes[2];
282 equal(trak2.type, 'trak', 'wrote a trak');
283 equal(trak2.boxes[0].trackId, 2, 'wrote the second track id');
284 equal(trak2.boxes[1].boxes[1].handlerType, 'soun', 'wrote the second track type');
285
286 mvex = init[1].boxes[3];
287 equal(mvex.type, 'mvex', 'wrote an mvex');
245 }); 288 });
246 289
247 test('generates a minimal moof', function() { 290 test('generates a minimal moof', function() {
......
...@@ -27,6 +27,8 @@ var ...@@ -27,6 +27,8 @@ var
27 parseStream, 27 parseStream,
28 ProgramStream = videojs.mp2t.ProgramStream, 28 ProgramStream = videojs.mp2t.ProgramStream,
29 programStream, 29 programStream,
30 H264Stream = videojs.mp2t.H264Stream,
31 h264Stream,
30 Transmuxer = videojs.mp2t.Transmuxer, 32 Transmuxer = videojs.mp2t.Transmuxer,
31 transmuxer, 33 transmuxer,
32 34
...@@ -37,7 +39,9 @@ var ...@@ -37,7 +39,9 @@ var
37 39
38 PAT, 40 PAT,
39 PMT, 41 PMT,
40 standalonePes; 42 standalonePes,
43
44 videoPes;
41 45
42 module('MP2T Packet Stream', { 46 module('MP2T Packet Stream', {
43 setup: function() { 47 setup: function() {
...@@ -261,8 +265,8 @@ PMT = [ ...@@ -261,8 +265,8 @@ PMT = [
261 0x40, 0x10, 265 0x40, 0x10,
262 // tsc:01 afc:01 cc:0000 pointer_field:0000 0000 266 // tsc:01 afc:01 cc:0000 pointer_field:0000 0000
263 0x50, 0x00, 267 0x50, 0x00,
264 // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0010 1111 268 // tid:0000 0010 ssi:0 0:0 r:00 sl:0000 0001 0111
265 0x00, 0x00, 0x2f, 269 0x02, 0x00, 0x17,
266 // pn:0000 0000 0000 0001 270 // pn:0000 0000 0000 0001
267 0x00, 0x01, 271 0x00, 0x01,
268 // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000 272 // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000
...@@ -292,12 +296,13 @@ test('parse the elementary streams from a program map table', function() { ...@@ -292,12 +296,13 @@ test('parse the elementary streams from a program map table', function() {
292 }); 296 });
293 parseStream.pmtPid = 0x0010; 297 parseStream.pmtPid = 0x0010;
294 298
295 parseStream.push(new Uint8Array(PMT)); 299 parseStream.push(new Uint8Array(PMT.concat(0, 0, 0, 0, 0)));
296 300
297 ok(packet, 'parsed a packet'); 301 ok(packet, 'parsed a packet');
298 ok(parseStream.programMapTable, 'parsed a program map'); 302 ok(parseStream.programMapTable, 'parsed a program map');
299 strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11'); 303 strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11');
300 strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12'); 304 strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12');
305 strictEqual(parseStream.programMapTable[0], undefined, 'ignored trailing stuffing bytes');
301 deepEqual(parseStream.programMapTable, packet.programMapTable, 'recorded the PMT'); 306 deepEqual(parseStream.programMapTable, packet.programMapTable, 'recorded the PMT');
302 }); 307 });
303 308
...@@ -386,57 +391,62 @@ test('parses an elementary stream packet with a pts and dts', function() { ...@@ -386,57 +391,62 @@ test('parses an elementary stream packet with a pts and dts', function() {
386 equal(2 / 90, packet.dts, 'parsed the dts'); 391 equal(2 / 90, packet.dts, 'parsed the dts');
387 }); 392 });
388 393
389 standalonePes = [ 394 // helper function to create video PES packets
390 0x47, // sync byte 395 videoPes = function(data) {
391 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 396 if (data.length !== 2) {
392 0x40, 0x11, 397 throw new Error('video PES only accepts 2 byte payloads');
393 // tsc:01 afc:11 cc:0000 398 }
394 0x70, 399 return [
395 // afl:1010 1100 400 0x47, // sync byte
396 0xac, 401 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001
397 // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0 402 0x40, 0x11,
398 0x00, 403 // tsc:01 afc:11 cc:0000
399 // stuffing_bytes (171 bytes) 404 0x70,
400 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 405 // afl:1010 1100
401 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 406 0xac,
402 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 407 // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0
403 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 408 0x00,
404 409 // stuffing_bytes (171 bytes)
405 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
406 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 411 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
407 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 412 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
408 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 413 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
409 414
410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 415 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
411 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 416 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
412 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 417 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
413 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 418 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
414 419
415 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 420 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
416 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 421 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
417 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 422 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
418 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 423 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
419 424
420 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 425 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
421 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 426 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
422 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 427 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
423 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 428 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
424 429
425 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 430 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
426 0xff, 0xff, 0xff, 431 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
427 // pscp:0000 0000 0000 0000 0000 0001 432 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
428 0x00, 0x00, 0x01, 433 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
429 // sid:0000 0000 ppl:0000 0000 0000 0101 434
430 0x00, 0x00, 0x05, 435 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
431 // 10 psc:00 pp:0 dai:1 c:0 ooc:0 436 0xff, 0xff, 0xff,
432 0x84, 437 // pscp:0000 0000 0000 0000 0000 0001
433 // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0 438 0x00, 0x00, 0x01,
434 0x20, 439 // sid:0000 0000 ppl:0000 0000 0000 0101
435 // phdl:0000 0000 440 0x00, 0x00, 0x05,
436 0x00, 441 // 10 psc:00 pp:0 dai:1 c:0 ooc:0
437 // "data":1010 1111 0000 0001 442 0x84,
438 0xaf, 0x01 443 // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0
439 ]; 444 0x20,
445 // phdl:0000 0000
446 0x00
447 ].concat(data);
448 };
449 standalonePes = videoPes([0xaf, 0x01]);
440 450
441 test('parses an elementary stream packet without a pts or dts', function() { 451 test('parses an elementary stream packet without a pts or dts', function() {
442 452
...@@ -503,10 +513,12 @@ test('parses metadata events from PSI packets', function() { ...@@ -503,10 +513,12 @@ test('parses metadata events from PSI packets', function() {
503 metadatas[0].tracks.sort(sortById); 513 metadatas[0].tracks.sort(sortById);
504 deepEqual(metadatas[0].tracks, [{ 514 deepEqual(metadatas[0].tracks, [{
505 id: 1, 515 id: 1,
506 codec: 'avc' 516 codec: 'avc',
517 type: 'video'
507 }, { 518 }, {
508 id: 2, 519 id: 2,
509 codec: 'adts' 520 codec: 'adts',
521 type: 'audio'
510 }], 'identified two tracks'); 522 }], 'identified two tracks');
511 }); 523 });
512 524
...@@ -628,6 +640,26 @@ test('flushes the buffered packets when a new one of that type is started', func ...@@ -628,6 +640,26 @@ test('flushes the buffered packets when a new one of that type is started', func
628 equal(7, packets[2].data.byteLength, 'parsed the audio payload'); 640 equal(7, packets[2].data.byteLength, 'parsed the audio payload');
629 }); 641 });
630 642
643 module('H264 Stream', {
644 setup: function() {
645 h264Stream = new H264Stream();
646 }
647 });
648 test('parses nal unit types', function() {
649 var data;
650 h264Stream.on('data', function(event) {
651 data = event;
652 });
653
654 h264Stream.push({
655 type: 'video',
656 data: new Uint8Array([0x09])
657 });
658
659 ok(data, 'generated a data event');
660 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter');
661 });
662
631 module('Transmuxer', { 663 module('Transmuxer', {
632 setup: function() { 664 setup: function() {
633 transmuxer = new Transmuxer(); 665 transmuxer = new Transmuxer();
...@@ -635,11 +667,48 @@ module('Transmuxer', { ...@@ -635,11 +667,48 @@ module('Transmuxer', {
635 }); 667 });
636 668
637 test('generates an init segment', function() { 669 test('generates an init segment', function() {
670 var segments = [];
671 transmuxer.on('data', function(segment) {
672 segments.push(segment);
673 });
638 transmuxer.push(packetize(PAT)); 674 transmuxer.push(packetize(PAT));
639 transmuxer.push(packetize(PMT)); 675 transmuxer.push(packetize(PMT));
640 transmuxer.push(packetize(standalonePes)); 676 transmuxer.push(packetize(standalonePes));
677 transmuxer.end();
641 678
642 ok(transmuxer.initSegment, 'has an init segment'); 679 equal(segments.length, 2, 'has an init segment');
680 });
681
682 test('buffers video samples until an access unit', function() {
683 var samples = [], boxes;
684 transmuxer.on('data', function(data) {
685 samples.push(data);
686 });
687 transmuxer.push(packetize(PAT));
688 transmuxer.push(packetize(PMT));
689
690 // buffer a NAL
691 transmuxer.push(packetize(videoPes([0x09, 0x01])));
692 transmuxer.push(packetize(videoPes([0x00, 0x02])));
693
694 // an access_unit_delimiter_rbsp should flush the buffer
695 transmuxer.push(packetize(videoPes([0x09, 0x03])));
696 transmuxer.push(packetize(videoPes([0x00, 0x04])));
697 equal(samples.length, 2, 'emitted two events');
698 boxes = videojs.inspectMp4(samples[1].data);
699 equal(boxes.length, 2, 'generated two boxes');
700 equal(boxes[0].type, 'moof', 'the first box is a moof');
701 equal(boxes[1].type, 'mdat', 'the second box is a mdat');
702 deepEqual(new Uint8Array(samples[1].data.subarray(samples[1].data.length - 4)),
703 new Uint8Array([0x09, 0x01, 0x00, 0x02]),
704 'concatenated NALs into an mdat');
705
706 // flush the last access unit
707 transmuxer.end();
708 equal(samples.length, 3, 'flushed the final access unit');
709 deepEqual(new Uint8Array(samples[2].data.subarray(samples[2].data.length - 4)),
710 new Uint8Array([0x09, 0x03, 0x00, 0x04]),
711 'concatenated NALs into an mdat');
643 }); 712 });
644 713
645 test('parses an example mp2t file and generates media segments', function() { 714 test('parses an example mp2t file and generates media segments', function() {
...@@ -653,23 +722,32 @@ test('parses an example mp2t file and generates media segments', function() { ...@@ -653,23 +722,32 @@ test('parses an example mp2t file and generates media segments', function() {
653 transmuxer.push(window.bcSegment); 722 transmuxer.push(window.bcSegment);
654 transmuxer.end(); 723 transmuxer.end();
655 724
656 ok(segments.length, 'generated media segments'); 725 equal(segments.length, 2, 'generated two segments');
657 i = segments.length; 726
658 while (i--) { 727 boxes = videojs.inspectMp4(segments[0].data);
659 boxes = videojs.inspectMp4(segments[i].data); 728 equal(boxes.length, 2, 'init segments are composed of two boxes');
660 equal(boxes.length, 2, 'segments are composed of two boxes'); 729 equal(boxes[0].type, 'ftyp', 'the first box is an ftyp');
661 equal(boxes[0].type, 'moof', 'first box is a moof'); 730 equal(boxes[1].type, 'moov', 'the second box is a moov');
662 equal(boxes[0].boxes.length, 2, 'the moof has two children'); 731 equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd');
663 732 equal(boxes[1].boxes[1].type, 'trak', 'generated a trak');
664 mfhd = boxes[0].boxes[0]; 733 equal(boxes[1].boxes[2].type, 'trak', 'generated a second trak');
734 equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex');
735
736 boxes = videojs.inspectMp4(segments[1].data);
737 ok(boxes.length > 0, 'media segments are not empty');
738 ok(boxes.length % 2 === 0, 'media segments are composed of pairs of boxes');
739 for (i = 0; i < boxes.length; i += 2) {
740 equal(boxes[i].type, 'moof', 'first box is a moof');
741 equal(boxes[i].boxes.length, 2, 'the moof has two children');
742
743 mfhd = boxes[i].boxes[0];
665 equal(mfhd.type, 'mfhd', 'mfhd is a child of the moof'); 744 equal(mfhd.type, 'mfhd', 'mfhd is a child of the moof');
666 ok(mfhd.sequenceNumber < sequenceNumber, 'sequence numbers are increasing'); 745 ok(mfhd.sequenceNumber < sequenceNumber, 'sequence numbers are increasing');
667 sequenceNumber = mfhd.sequenceNumber; 746 sequenceNumber = mfhd.sequenceNumber;
668 747
669 traf = boxes[0].boxes[1]; 748 traf = boxes[i].boxes[1];
670 equal(traf.type, 'traf', 'traf is a child of the moof'); 749 equal(traf.type, 'traf', 'traf is a child of the moof');
671 750 equal(boxes[i + 1].type, 'mdat', 'second box is an mdat');
672 equal(boxes[1].type, 'mdat', 'second box is an mdat');
673 } 751 }
674 }); 752 });
675 753
......