55ac868e by David LaPalomento

Fill out media dimensions in init segment

Add in a parser to unpack NAL byte streams. Hook up the old exponential Golomb decoder and parse media metadata out of the first sequence parameter set. Add more checks to test on the example segment transformation.
1 parent 1b73bdb0
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
14 (function(window, videojs, undefined) { 14 (function(window, videojs, undefined) {
15 'use strict'; 15 'use strict';
16 16
17 var PacketStream, ParseStream, ProgramStream, Transmuxer, AacStream, H264Stream, MP2T_PACKET_LENGTH, H264_STREAM_TYPE, ADTS_STREAM_TYPE, mp4; 17 var PacketStream, ParseStream, ProgramStream, Transmuxer, AacStream, H264Stream, NalByteStream, MP2T_PACKET_LENGTH, H264_STREAM_TYPE, ADTS_STREAM_TYPE, mp4;
18 18
19 MP2T_PACKET_LENGTH = 188; // bytes 19 MP2T_PACKET_LENGTH = 188; // bytes
20 H264_STREAM_TYPE = 0x1b; 20 H264_STREAM_TYPE = 0x1b;
...@@ -285,6 +285,7 @@ ProgramStream = function() { ...@@ -285,6 +285,7 @@ ProgramStream = function() {
285 if (!stream.data.length) { 285 if (!stream.data.length) {
286 return; 286 return;
287 } 287 }
288 event.trackId = stream.data[0].pid;
288 289
289 // reassemble the packet 290 // reassemble the packet
290 while (stream.data.length) { 291 while (stream.data.length) {
...@@ -394,11 +395,84 @@ AacStream = function() { ...@@ -394,11 +395,84 @@ AacStream = function() {
394 AacStream.prototype = new videojs.Hls.Stream(); 395 AacStream.prototype = new videojs.Hls.Stream();
395 396
396 /** 397 /**
398 * Accepts a NAL unit byte stream and unpacks the embedded NAL units.
399 */
400 NalByteStream = function() {
401 var
402 i = 6,
403 // the first NAL unit is prefixed by an extra zero byte
404 syncPoint = 1,
405 buffer;
406 NalByteStream.prototype.init.call(this);
407
408 this.push = function(data) {
409 var swapBuffer;
410
411 if (!buffer) {
412 buffer = data.data;
413 } else {
414 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
415 swapBuffer.set(buffer);
416 swapBuffer.set(data.data, buffer.byteLength);
417 buffer = swapBuffer;
418 }
419
420 // scan for synchronization byte sequences (0x00 00 01)
421
422 // a match looks like this:
423 // 0 0 1 .. NAL .. 0 0 1
424 // ^ sync point ^ i
425 while (i < buffer.byteLength) {
426 switch (buffer[i]) {
427 case 0:
428 i++;
429 break;
430 case 1:
431 // skip past non-sync sequences
432 if (buffer[i - 1] !== 0 ||
433 buffer[i - 2] !== 0) {
434 i += 3;
435 break;
436 }
437
438 // deliver the NAL unit
439 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
440 syncPoint = i - 2;
441 i += 3;
442 break;
443 default:
444 i += 3;
445 break;
446 }
447 }
448 // filter out the NAL units that were delivered
449 buffer = buffer.subarray(syncPoint);
450 i -= syncPoint;
451 syncPoint = 0;
452 };
453
454 this.end = function() {
455 // deliver the last buffered NAL unit
456 if (buffer.byteLength > 3) {
457 this.trigger('data', buffer.subarray(syncPoint + 3));
458 }
459 };
460 };
461 NalByteStream.prototype = new videojs.Hls.Stream();
462
463 /**
397 * Accepts a ProgramStream and emits data events with parsed 464 * Accepts a ProgramStream and emits data events with parsed
398 * AAC Audio Frames of the individual packets. 465 * AAC Audio Frames of the individual packets.
399 */ 466 */
400 H264Stream = function() { 467 H264Stream = function() {
401 var self; 468 var
469 nalByteStream = new NalByteStream(),
470 self,
471 trackId,
472
473 readSequenceParameterSet,
474 skipScalingList;
475
402 H264Stream.prototype.init.call(this); 476 H264Stream.prototype.init.call(this);
403 self = this; 477 self = this;
404 478
...@@ -406,16 +480,159 @@ H264Stream = function() { ...@@ -406,16 +480,159 @@ H264Stream = function() {
406 if (packet.type !== 'video') { 480 if (packet.type !== 'video') {
407 return; 481 return;
408 } 482 }
409 switch (packet.data[0]) { 483 trackId = packet.trackId;
484
485 nalByteStream.push(packet);
486 };
487
488 nalByteStream.on('data', function(data) {
489 var event = {
490 trackId: trackId,
491 data: data
492 };
493 switch (data[0] & 0x1f) {
410 case 0x09: 494 case 0x09:
411 packet.nalUnitType = 'access_unit_delimiter_rbsp'; 495 event.nalUnitType = 'access_unit_delimiter_rbsp';
496 break;
497
498 case 0x07:
499 event.nalUnitType = 'seq_parameter_set_rbsp';
500 event.dimensions = readSequenceParameterSet(data.subarray(1));
412 break; 501 break;
413 502
414 default: 503 default:
415 break; 504 break;
416 } 505 }
417 this.trigger('data', packet); 506 self.trigger('data', event);
507 });
508
509 this.end = function() {
510 nalByteStream.end();
511 };
512
513 /**
514 * Advance the ExpGolomb decoder past a scaling list. The scaling
515 * list is optionally transmitted as part of a sequence parameter
516 * set and is not relevant to transmuxing.
517 * @param count {number} the number of entries in this scaling list
518 * @param expGolombDecoder {object} an ExpGolomb pointed to the
519 * start of a scaling list
520 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
521 */
522 skipScalingList = function(count, expGolombDecoder) {
523 var
524 lastScale = 8,
525 nextScale = 8,
526 j,
527 deltaScale;
528
529 for (j = 0; j < count; j++) {
530 if (nextScale !== 0) {
531 deltaScale = expGolombDecoder.readExpGolomb();
532 nextScale = (lastScale + deltaScale + 256) % 256;
533 }
534
535 lastScale = (nextScale === 0) ? lastScale : nextScale;
536 }
537 };
538
539 /**
540 * Read a sequence parameter set and return some interesting video
541 * properties. A sequence parameter set is the H264 metadata that
542 * describes the properties of upcoming video frames.
543 * @param data {Uint8Array} the bytes of a sequence parameter set
544 * @return {object} an object with width and height properties
545 * specifying the dimensions of the associated video frames.
546 */
547 readSequenceParameterSet = function(data) {
548 var
549 frameCropLeftOffset = 0,
550 frameCropRightOffset = 0,
551 frameCropTopOffset = 0,
552 frameCropBottomOffset = 0,
553 expGolombDecoder, profileIdc, chromaFormatIdc, picOrderCntType,
554 numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1,
555 picHeightInMapUnitsMinus1, frameMbsOnlyFlag,
556 scalingListCount,
557 i;
558
559 expGolombDecoder = new videojs.Hls.ExpGolomb(data);
560 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
561 // constraint_set[0-5]_flag, u(1), reserved_zero_2bits u(2), level_idc u()8
562 expGolombDecoder.skipBits(16);
563 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
564
565 // some profiles have more optional data we don't need
566 if (profileIdc === 100 ||
567 profileIdc === 110 ||
568 profileIdc === 122 ||
569 profileIdc === 244 ||
570 profileIdc === 44 ||
571 profileIdc === 83 ||
572 profileIdc === 86 ||
573 profileIdc === 118 ||
574 profileIdc === 128) {
575 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
576 if (chromaFormatIdc === 3) {
577 expGolombDecoder.skipBits(1); // separate_colour_plane_flag
578 }
579 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
580 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
581 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
582 if (expGolombDecoder.readBoolean()) { // seq_scaling_matrix_present_flag
583 scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12;
584 for (i = 0; i < scalingListCount; i++) {
585 if (expGolombDecoder.readBoolean()) { // seq_scaling_list_present_flag[ i ]
586 if (i < 6) {
587 skipScalingList(16, expGolombDecoder);
588 } else {
589 skipScalingList(64, expGolombDecoder);
590 }
591 }
592 }
593 }
594 }
595
596 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
597 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
598
599 if (picOrderCntType === 0) {
600 expGolombDecoder.readUnsignedExpGolomb(); //log2_max_pic_order_cnt_lsb_minus4
601 } else if (picOrderCntType === 1) {
602 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
603 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
604 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
605 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
606 for(i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
607 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
608 }
609 }
610
611 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
612 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
613
614 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
615 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
616
617 frameMbsOnlyFlag = expGolombDecoder.readBits(1);
618 if (frameMbsOnlyFlag === 0) {
619 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
620 }
621
622 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
623 if (expGolombDecoder.readBoolean()) { // frame_cropping_flag
624 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
625 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
626 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
627 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
628 }
629
630 return {
631 width: ((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2,
632 height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - (frameCropTopOffset * 2) - (frameCropBottomOffset * 2)
633 };
418 }; 634 };
635
419 }; 636 };
420 H264Stream.prototype = new videojs.Hls.Stream(); 637 H264Stream.prototype = new videojs.Hls.Stream();
421 638
...@@ -424,9 +641,10 @@ Transmuxer = function() { ...@@ -424,9 +641,10 @@ Transmuxer = function() {
424 var 641 var
425 self = this, 642 self = this,
426 sequenceNumber = 0, 643 sequenceNumber = 0,
427 initialized = false,
428 videoSamples = [], 644 videoSamples = [],
429 videoSamplesSize = 0, 645 videoSamplesSize = 0,
646 tracks,
647 dimensions,
430 648
431 packetStream, parseStream, programStream, aacStream, h264Stream, 649 packetStream, parseStream, programStream, aacStream, h264Stream,
432 650
...@@ -446,6 +664,42 @@ Transmuxer = function() { ...@@ -446,6 +664,42 @@ Transmuxer = function() {
446 programStream.pipe(aacStream); 664 programStream.pipe(aacStream);
447 programStream.pipe(h264Stream); 665 programStream.pipe(h264Stream);
448 666
667 // handle incoming data events
668 h264Stream.on('data', function(data) {
669 var i;
670
671 // if this chunk starts a new access unit, flush the data we've been buffering
672 if (data.nalUnitType === 'access_unit_delimiter_rbsp' &&
673 videoSamples.length) {
674 //flushVideo();
675 }
676 // generate an init segment once all the metadata is available
677 if (data.nalUnitType === 'seq_parameter_set_rbsp' &&
678 !dimensions) {
679 dimensions = data.dimensions;
680
681 i = tracks.length;
682 while (i--) {
683 if (tracks[i].type === 'video') {
684 tracks[i].width = dimensions.width;
685 tracks[i].height = dimensions.height;
686 }
687 }
688 self.trigger('data', {
689 data: videojs.mp4.initSegment(tracks)
690 });
691 }
692
693 // buffer video until we encounter a new access unit (aka the next frame)
694 videoSamples.push(data);
695 videoSamplesSize += data.data.byteLength;
696 });
697 programStream.on('data', function(data) {
698 if (data.type === 'metadata') {
699 tracks = data.tracks;
700 }
701 });
702
449 // helper functions 703 // helper functions
450 flushVideo = function() { 704 flushVideo = function() {
451 var moof, mdat, boxes, i, data; 705 var moof, mdat, boxes, i, data;
...@@ -478,28 +732,6 @@ Transmuxer = function() { ...@@ -478,28 +732,6 @@ Transmuxer = function() {
478 }); 732 });
479 }; 733 };
480 734
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 }
501 });
502
503 // feed incoming data to the front of the parsing pipeline 735 // feed incoming data to the front of the parsing pipeline
504 this.push = function(data) { 736 this.push = function(data) {
505 packetStream.push(data); 737 packetStream.push(data);
...@@ -507,6 +739,7 @@ Transmuxer = function() { ...@@ -507,6 +739,7 @@ Transmuxer = function() {
507 // flush any buffered data 739 // flush any buffered data
508 this.end = function() { 740 this.end = function() {
509 programStream.end(); 741 programStream.end();
742 h264Stream.end();
510 if (videoSamples.length) { 743 if (videoSamples.length) {
511 flushVideo(); 744 flushVideo();
512 } 745 }
......
...@@ -40,6 +40,7 @@ var ...@@ -40,6 +40,7 @@ var
40 PAT, 40 PAT,
41 PMT, 41 PMT,
42 standalonePes, 42 standalonePes,
43 validateTrack,
43 44
44 videoPes; 45 videoPes;
45 46
...@@ -392,48 +393,31 @@ test('parses an elementary stream packet with a pts and dts', function() { ...@@ -392,48 +393,31 @@ test('parses an elementary stream packet with a pts and dts', function() {
392 }); 393 });
393 394
394 // helper function to create video PES packets 395 // helper function to create video PES packets
395 videoPes = function(data) { 396 videoPes = function(data, first) {
396 if (data.length !== 2) { 397 var
397 throw new Error('video PES only accepts 2 byte payloads'); 398 adaptationFieldLength = 188 - data.length - (first ? 18 : 17),
399 result = [
400 // sync byte
401 0x47,
402 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001
403 0x40, 0x11,
404 // tsc:01 afc:11 cc:0000
405 0x70
406 ].concat([
407 // afl
408 adaptationFieldLength & 0xff,
409 // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0
410 0x00
411 ]),
412 i;
413
414 i = adaptationFieldLength - 1;
415 while (i--) {
416 // stuffing_bytes
417 result.push(0xff);
398 } 418 }
399 return [ 419
400 0x47, // sync byte 420 result = result.concat([
401 // tei:0 pusi:1 tp:0 pid:0 0000 0001 0001
402 0x40, 0x11,
403 // tsc:01 afc:11 cc:0000
404 0x70,
405 // afl:1010 1100
406 0xac,
407 // di:0 rai:0 espi:0 pf:0 of:0 spf:0 tpdf:0 afef:0
408 0x00,
409 // stuffing_bytes (171 bytes)
410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
411 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
412 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
413 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
414
415 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
416 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
417 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
418 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
419
420 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
421 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
422 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
423 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
424
425 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
426 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
427 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
428 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
429
430 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
431 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
432 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
433 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
434
435 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
436 0xff, 0xff, 0xff,
437 // pscp:0000 0000 0000 0000 0000 0001 421 // pscp:0000 0000 0000 0000 0000 0001
438 0x00, 0x00, 0x01, 422 0x00, 0x00, 0x01,
439 // sid:0000 0000 ppl:0000 0000 0000 0101 423 // sid:0000 0000 ppl:0000 0000 0000 0101
...@@ -444,9 +428,17 @@ videoPes = function(data) { ...@@ -444,9 +428,17 @@ videoPes = function(data) {
444 0x20, 428 0x20,
445 // phdl:0000 0000 429 // phdl:0000 0000
446 0x00 430 0x00
447 ].concat(data); 431 ]);
432 if (first) {
433 result.push(0x00);
434 }
435 result = result.concat([
436 // NAL unit start code
437 0x00, 0x00, 0x01
438 ].concat(data));
439 return result;
448 }; 440 };
449 standalonePes = videoPes([0xaf, 0x01]); 441 standalonePes = videoPes([0xaf, 0x01], true);
450 442
451 test('parses an elementary stream packet without a pts or dts', function() { 443 test('parses an elementary stream packet without a pts or dts', function() {
452 444
...@@ -465,9 +457,9 @@ test('parses an elementary stream packet without a pts or dts', function() { ...@@ -465,9 +457,9 @@ test('parses an elementary stream packet without a pts or dts', function() {
465 ok(packet, 'parsed a packet'); 457 ok(packet, 'parsed a packet');
466 equal('pes', packet.type, 'recognized a PES packet'); 458 equal('pes', packet.type, 'recognized a PES packet');
467 equal(0x1b, packet.streamType, 'tracked the stream_type'); 459 equal(0x1b, packet.streamType, 'tracked the stream_type');
468 equal(2, packet.data.byteLength, 'parsed two data bytes'); 460 equal(2 + 4, packet.data.byteLength, 'parsed two data bytes');
469 equal(0xaf, packet.data[0], 'parsed the first data byte'); 461 equal(0xaf, packet.data[packet.data.length - 2], 'parsed the first data byte');
470 equal(0x01, packet.data[1], 'parsed the second data byte'); 462 equal(0x01, packet.data[packet.data.length - 1], 'parsed the second data byte');
471 ok(!packet.pts, 'did not parse a pts'); 463 ok(!packet.pts, 'did not parse a pts');
472 ok(!packet.dts, 'did not parse a dts'); 464 ok(!packet.dts, 'did not parse a dts');
473 }); 465 });
...@@ -645,6 +637,85 @@ module('H264 Stream', { ...@@ -645,6 +637,85 @@ module('H264 Stream', {
645 h264Stream = new H264Stream(); 637 h264Stream = new H264Stream();
646 } 638 }
647 }); 639 });
640
641 test('unpacks nal units from simple byte stream framing', function() {
642 var data;
643 h264Stream.on('data', function(event) {
644 data = event;
645 });
646
647 // the simplest byte stream framing:
648 h264Stream.push({
649 type: 'video',
650 data: new Uint8Array([
651 0x00, 0x00, 0x00, 0x01,
652 0x09, 0x07,
653 0x00, 0x00, 0x01
654 ])
655 });
656
657 ok(data, 'generated a data event');
658 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter');
659 equal(data.data.length, 2, 'calculated nal unit length');
660 equal(data.data[1], 7, 'read a payload byte');
661 });
662
663 test('unpacks nal units from byte streams split across pushes', function() {
664 var data;
665 h264Stream.on('data', function(event) {
666 data = event;
667 });
668
669 // handles byte streams split across pushes
670 h264Stream.push({
671 type: 'video',
672 data: new Uint8Array([
673 0x00, 0x00, 0x00, 0x01,
674 0x09])
675 });
676 ok(!data, 'buffers NAL units across events');
677
678 h264Stream.push({
679 type: 'video',
680 data: new Uint8Array([
681 0x07,
682 0x00, 0x00, 0x01
683 ])
684 });
685 ok(data, 'generated a data event');
686 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter');
687 equal(data.data.length, 2, 'calculated nal unit length');
688 equal(data.data[1], 7, 'read a payload byte');
689 });
690
691 test('unpacks nal units from byte streams with split sync points', function() {
692 var data;
693 h264Stream.on('data', function(event) {
694 data = event;
695 });
696
697 // handles sync points split across pushes
698 h264Stream.push({
699 type: 'video',
700 data: new Uint8Array([
701 0x00, 0x00, 0x00, 0x01,
702 0x09, 0x07,
703 0x00])
704 });
705 ok(!data, 'buffers NAL units across events');
706
707 h264Stream.push({
708 type: 'video',
709 data: new Uint8Array([
710 0x00, 0x01
711 ])
712 });
713 ok(data, 'generated a data event');
714 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter');
715 equal(data.data.length, 2, 'calculated nal unit length');
716 equal(data.data[1], 7, 'read a payload byte');
717 });
718
648 test('parses nal unit types', function() { 719 test('parses nal unit types', function() {
649 var data; 720 var data;
650 h264Stream.on('data', function(event) { 721 h264Stream.on('data', function(event) {
...@@ -653,11 +724,32 @@ test('parses nal unit types', function() { ...@@ -653,11 +724,32 @@ test('parses nal unit types', function() {
653 724
654 h264Stream.push({ 725 h264Stream.push({
655 type: 'video', 726 type: 'video',
656 data: new Uint8Array([0x09]) 727 data: new Uint8Array([
728 0x00, 0x00, 0x00, 0x01,
729 0x09
730 ])
657 }); 731 });
732 h264Stream.end();
658 733
659 ok(data, 'generated a data event'); 734 ok(data, 'generated a data event');
660 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter'); 735 equal(data.nalUnitType, 'access_unit_delimiter_rbsp', 'identified an access unit delimiter');
736
737 data = null;
738 h264Stream.push({
739 type: 'video',
740 data: new Uint8Array([
741 0x00, 0x00, 0x00, 0x01,
742 0x07,
743 0x27, 0x42, 0xe0, 0x0b,
744 0xa9, 0x18, 0x60, 0x9d,
745 0x80, 0x35, 0x06, 0x01,
746 0x06, 0xb6, 0xc2, 0xb5,
747 0xef, 0x7c, 0x04
748 ])
749 });
750 h264Stream.end();
751 ok(data, 'generated a data event');
752 equal(data.nalUnitType, 'seq_parameter_set_rbsp', 'identified a sequence parameter set');
661 }); 753 });
662 754
663 module('Transmuxer', { 755 module('Transmuxer', {
...@@ -673,13 +765,20 @@ test('generates an init segment', function() { ...@@ -673,13 +765,20 @@ test('generates an init segment', function() {
673 }); 765 });
674 transmuxer.push(packetize(PAT)); 766 transmuxer.push(packetize(PAT));
675 transmuxer.push(packetize(PMT)); 767 transmuxer.push(packetize(PMT));
676 transmuxer.push(packetize(standalonePes)); 768 transmuxer.push(packetize(videoPes([
769 0x07,
770 0x27, 0x42, 0xe0, 0x0b,
771 0xa9, 0x18, 0x60, 0x9d,
772 0x80, 0x53, 0x06, 0x01,
773 0x06, 0xb6, 0xc2, 0xb5,
774 0xef, 0x7c, 0x04
775 ], true)));
677 transmuxer.end(); 776 transmuxer.end();
678 777
679 equal(segments.length, 2, 'has an init segment'); 778 equal(segments.length, 2, 'has an init segment');
680 }); 779 });
681 780
682 test('buffers video samples until an access unit', function() { 781 test('buffers video samples until ended', function() {
683 var samples = [], boxes; 782 var samples = [], boxes;
684 transmuxer.on('data', function(data) { 783 transmuxer.on('data', function(data) {
685 samples.push(data); 784 samples.push(data);
...@@ -688,34 +787,68 @@ test('buffers video samples until an access unit', function() { ...@@ -688,34 +787,68 @@ test('buffers video samples until an access unit', function() {
688 transmuxer.push(packetize(PMT)); 787 transmuxer.push(packetize(PMT));
689 788
690 // buffer a NAL 789 // buffer a NAL
691 transmuxer.push(packetize(videoPes([0x09, 0x01]))); 790 transmuxer.push(packetize(videoPes([0x09, 0x01], true)));
692 transmuxer.push(packetize(videoPes([0x00, 0x02]))); 791 transmuxer.push(packetize(videoPes([0x00, 0x02])));
693 792
694 // an access_unit_delimiter_rbsp should flush the buffer 793 // add an access_unit_delimiter_rbsp
695 transmuxer.push(packetize(videoPes([0x09, 0x03]))); 794 transmuxer.push(packetize(videoPes([0x09, 0x03])));
696 transmuxer.push(packetize(videoPes([0x00, 0x04]))); 795 transmuxer.push(packetize(videoPes([0x00, 0x04])));
697 equal(samples.length, 2, 'emitted two events'); 796 transmuxer.push(packetize(videoPes([0x00, 0x05])));
698 boxes = videojs.inspectMp4(samples[1].data); 797
798 // flush everything
799 transmuxer.end();
800 equal(samples.length, 1, 'emitted one event');
801 boxes = videojs.inspectMp4(samples[0].data);
699 equal(boxes.length, 2, 'generated two boxes'); 802 equal(boxes.length, 2, 'generated two boxes');
700 equal(boxes[0].type, 'moof', 'the first box is a moof'); 803 equal(boxes[0].type, 'moof', 'the first box is a moof');
701 equal(boxes[1].type, 'mdat', 'the second box is a mdat'); 804 equal(boxes[1].type, 'mdat', 'the second box is a mdat');
702 deepEqual(new Uint8Array(samples[1].data.subarray(samples[1].data.length - 4)), 805 deepEqual(new Uint8Array(samples[0].data.subarray(samples[0].data.length - 10)),
703 new Uint8Array([0x09, 0x01, 0x00, 0x02]), 806 new Uint8Array([
704 'concatenated NALs into an mdat'); 807 0x09, 0x01,
705 808 0x00, 0x02,
706 // flush the last access unit 809 0x09, 0x03,
707 transmuxer.end(); 810 0x00, 0x04,
708 equal(samples.length, 3, 'flushed the final access unit'); 811 0x00, 0x05]),
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'); 812 'concatenated NALs into an mdat');
712 }); 813 });
713 814
815 validateTrack = function(track, metadata) {
816 var mdia, handlerType;
817 equal(track.type, 'trak', 'wrote the track type');
818 equal(track.boxes.length, 2, 'wrote track children');
819 equal(track.boxes[0].type, 'tkhd', 'wrote the track header');
820 if (metadata) {
821 if (metadata.trackId) {
822 equal(track.boxes[0].trackId, metadata.trackId, 'wrote the track id');
823 }
824 if (metadata.width) {
825 equal(track.boxes[0].width, metadata.width, 'wrote the width');
826 }
827 if (metadata.height) {
828 equal(track.boxes[0].height, metadata.height, 'wrote the height');
829 }
830 }
831
832 mdia = track.boxes[1];
833 equal(mdia.type, 'mdia', 'wrote the media');
834 equal(mdia.boxes.length, 3, 'wrote the mdia children');
835
836 equal(mdia.boxes[0].type, 'mdhd', 'wrote the media header');
837 equal(mdia.boxes[0].language, 'und', 'the language is undefined');
838 equal(mdia.boxes[0].duration, 0xffffffff, 'the duration is at maximum');
839
840 equal(mdia.boxes[1].type, 'hdlr', 'wrote the media handler');
841 handlerType = mdia.boxes[1].handlerType;
842
843 equal(mdia.boxes[2].type, 'minf', 'wrote the media info');
844 };
845
714 test('parses an example mp2t file and generates media segments', function() { 846 test('parses an example mp2t file and generates media segments', function() {
715 var 847 var
716 segments = [], 848 segments = [],
717 sequenceNumber = window.Infinity, 849 sequenceNumber = window.Infinity,
718 i, boxes, mfhd, traf; 850 i, boxes, mfhd, traf;
851
719 transmuxer.on('data', function(segment) { 852 transmuxer.on('data', function(segment) {
720 segments.push(segment); 853 segments.push(segment);
721 }); 854 });
...@@ -729,8 +862,14 @@ test('parses an example mp2t file and generates media segments', function() { ...@@ -729,8 +862,14 @@ test('parses an example mp2t file and generates media segments', function() {
729 equal(boxes[0].type, 'ftyp', 'the first box is an ftyp'); 862 equal(boxes[0].type, 'ftyp', 'the first box is an ftyp');
730 equal(boxes[1].type, 'moov', 'the second box is a moov'); 863 equal(boxes[1].type, 'moov', 'the second box is a moov');
731 equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd'); 864 equal(boxes[1].boxes[0].type, 'mvhd', 'generated an mvhd');
732 equal(boxes[1].boxes[1].type, 'trak', 'generated a trak'); 865 validateTrack(boxes[1].boxes[1], {
733 equal(boxes[1].boxes[2].type, 'trak', 'generated a second trak'); 866 trackId: 256,
867 width: 388,
868 height: 300
869 });
870 validateTrack(boxes[1].boxes[2], {
871 trackId: 257
872 });
734 equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex'); 873 equal(boxes[1].boxes[3].type, 'mvex', 'generated an mvex');
735 874
736 boxes = videojs.inspectMp4(segments[1].data); 875 boxes = videojs.inspectMp4(segments[1].data);
......