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,13 +446,23 @@ Transmuxer = function() { ...@@ -439,13 +446,23 @@ 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;
452
453 moof = mp4.moof(sequenceNumber, []);
454
455 // concatenate the video data and construct the mdat
456 data = new Uint8Array(videoSamplesSize);
457 i = 0;
458 while (videoSamples.length) {
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);
444 465
445 h264Stream.on('data', function(data) {
446 var
447 moof = mp4.moof(sequenceNumber, []),
448 mdat = mp4.mdat(data.data),
449 // it would be great to allocate this array up front instead of 466 // it would be great to allocate this array up front instead of
450 // throwing away hundreds of media segment fragments 467 // throwing away hundreds of media segment fragments
451 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 468 boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
...@@ -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;
462 }); 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 }
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,7 +391,12 @@ test('parses an elementary stream packet with a pts and dts', function() { ...@@ -386,7 +391,12 @@ 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
395 videoPes = function(data) {
396 if (data.length !== 2) {
397 throw new Error('video PES only accepts 2 byte payloads');
398 }
399 return [
390 0x47, // sync byte 400 0x47, // sync byte
391 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001 401 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001
392 0x40, 0x11, 402 0x40, 0x11,
...@@ -433,10 +443,10 @@ standalonePes = [ ...@@ -433,10 +443,10 @@ standalonePes = [
433 // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0 443 // pdf:00 ef:1 erf:0 dtmf:0 acif:0 pcf:0 pef:0
434 0x20, 444 0x20,
435 // phdl:0000 0000 445 // phdl:0000 0000
436 0x00, 446 0x00
437 // "data":1010 1111 0000 0001 447 ].concat(data);
438 0xaf, 0x01 448 };
439 ]; 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
......