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.
Showing
2 changed files
with
464 additions
and
92 deletions
... | @@ -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); | ... | ... |
-
Please register or sign in to post a comment