490 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			490 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * mux.js
 | |
|  *
 | |
|  * Copyright (c) Brightcove
 | |
|  * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
 | |
|  */
 | |
| 'use strict';
 | |
| 
 | |
| var Stream = require('../utils/stream.js');
 | |
| var ExpGolomb = require('../utils/exp-golomb.js');
 | |
| 
 | |
| var H264Stream, NalByteStream;
 | |
| var PROFILES_WITH_OPTIONAL_SPS_DATA;
 | |
| 
 | |
| /**
 | |
|  * Accepts a NAL unit byte stream and unpacks the embedded NAL units.
 | |
|  */
 | |
| NalByteStream = function() {
 | |
|   var
 | |
|     syncPoint = 0,
 | |
|     i,
 | |
|     buffer;
 | |
|   NalByteStream.prototype.init.call(this);
 | |
| 
 | |
|   /*
 | |
|    * Scans a byte stream and triggers a data event with the NAL units found.
 | |
|    * @param {Object} data Event received from H264Stream
 | |
|    * @param {Uint8Array} data.data The h264 byte stream to be scanned
 | |
|    *
 | |
|    * @see H264Stream.push
 | |
|    */
 | |
|   this.push = function(data) {
 | |
|     var swapBuffer;
 | |
| 
 | |
|     if (!buffer) {
 | |
|       buffer = data.data;
 | |
|     } else {
 | |
|       swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
 | |
|       swapBuffer.set(buffer);
 | |
|       swapBuffer.set(data.data, buffer.byteLength);
 | |
|       buffer = swapBuffer;
 | |
|     }
 | |
|     var len = buffer.byteLength;
 | |
| 
 | |
|     // Rec. ITU-T H.264, Annex B
 | |
|     // scan for NAL unit boundaries
 | |
| 
 | |
|     // a match looks like this:
 | |
|     // 0 0 1 .. NAL .. 0 0 1
 | |
|     // ^ sync point        ^ i
 | |
|     // or this:
 | |
|     // 0 0 1 .. NAL .. 0 0 0
 | |
|     // ^ sync point        ^ i
 | |
| 
 | |
|     // advance the sync point to a NAL start, if necessary
 | |
|     for (; syncPoint < len - 3; syncPoint++) {
 | |
|       if (buffer[syncPoint + 2] === 1) {
 | |
|         // the sync point is properly aligned
 | |
|         i = syncPoint + 5;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     while (i < len) {
 | |
|       // look at the current byte to determine if we've hit the end of
 | |
|       // a NAL unit boundary
 | |
|       switch (buffer[i]) {
 | |
|       case 0:
 | |
|         // skip past non-sync sequences
 | |
|         if (buffer[i - 1] !== 0) {
 | |
|           i += 2;
 | |
|           break;
 | |
|         } else if (buffer[i - 2] !== 0) {
 | |
|           i++;
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // deliver the NAL unit if it isn't empty
 | |
|         if (syncPoint + 3 !== i - 2) {
 | |
|           this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
 | |
|         }
 | |
| 
 | |
|         // drop trailing zeroes
 | |
|         do {
 | |
|           i++;
 | |
|         } while (buffer[i] !== 1 && i < len);
 | |
|         syncPoint = i - 2;
 | |
|         i += 3;
 | |
|         break;
 | |
|       case 1:
 | |
|         // skip past non-sync sequences
 | |
|         if (buffer[i - 1] !== 0 ||
 | |
|             buffer[i - 2] !== 0) {
 | |
|           i += 3;
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // deliver the NAL unit
 | |
|         this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
 | |
|         syncPoint = i - 2;
 | |
|         i += 3;
 | |
|         break;
 | |
|       default:
 | |
|         // the current byte isn't a one or zero, so it cannot be part
 | |
|         // of a sync sequence
 | |
|         i += 3;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     // filter out the NAL units that were delivered
 | |
|     buffer = buffer.subarray(syncPoint);
 | |
|     i -= syncPoint;
 | |
|     syncPoint = 0;
 | |
|   };
 | |
| 
 | |
|   this.reset = function() {
 | |
|     buffer = null;
 | |
|     syncPoint = 0;
 | |
|     this.trigger('reset');
 | |
|   };
 | |
| 
 | |
|   this.flush = function() {
 | |
|     // deliver the last buffered NAL unit
 | |
|     if (buffer && buffer.byteLength > 3) {
 | |
|       this.trigger('data', buffer.subarray(syncPoint + 3));
 | |
|     }
 | |
|     // reset the stream state
 | |
|     buffer = null;
 | |
|     syncPoint = 0;
 | |
|     this.trigger('done');
 | |
|   };
 | |
| 
 | |
|   this.endTimeline = function() {
 | |
|     this.flush();
 | |
|     this.trigger('endedtimeline');
 | |
|   };
 | |
| };
 | |
| NalByteStream.prototype = new Stream();
 | |
| 
 | |
| // values of profile_idc that indicate additional fields are included in the SPS
 | |
| // see Recommendation ITU-T H.264 (4/2013),
 | |
| // 7.3.2.1.1 Sequence parameter set data syntax
 | |
| PROFILES_WITH_OPTIONAL_SPS_DATA = {
 | |
|   100: true,
 | |
|   110: true,
 | |
|   122: true,
 | |
|   244: true,
 | |
|   44: true,
 | |
|   83: true,
 | |
|   86: true,
 | |
|   118: true,
 | |
|   128: true,
 | |
| 
 | |
|   // TODO: the three profiles below don't
 | |
|   // appear to have sps data in the specificiation anymore?
 | |
|   138: true,
 | |
|   139: true,
 | |
|   134: true
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Accepts input from a ElementaryStream and produces H.264 NAL unit data
 | |
|  * events.
 | |
|  */
 | |
| H264Stream = function() {
 | |
|   var
 | |
|     nalByteStream = new NalByteStream(),
 | |
|     self,
 | |
|     trackId,
 | |
|     currentPts,
 | |
|     currentDts,
 | |
| 
 | |
|     discardEmulationPreventionBytes,
 | |
|     readSequenceParameterSet,
 | |
|     skipScalingList;
 | |
| 
 | |
|   H264Stream.prototype.init.call(this);
 | |
|   self = this;
 | |
| 
 | |
|   /*
 | |
|    * Pushes a packet from a stream onto the NalByteStream
 | |
|    *
 | |
|    * @param {Object} packet - A packet received from a stream
 | |
|    * @param {Uint8Array} packet.data - The raw bytes of the packet
 | |
|    * @param {Number} packet.dts - Decode timestamp of the packet
 | |
|    * @param {Number} packet.pts - Presentation timestamp of the packet
 | |
|    * @param {Number} packet.trackId - The id of the h264 track this packet came from
 | |
|    * @param {('video'|'audio')} packet.type - The type of packet
 | |
|    *
 | |
|    */
 | |
|   this.push = function(packet) {
 | |
|     if (packet.type !== 'video') {
 | |
|       return;
 | |
|     }
 | |
|     trackId = packet.trackId;
 | |
|     currentPts = packet.pts;
 | |
|     currentDts = packet.dts;
 | |
| 
 | |
|     nalByteStream.push(packet);
 | |
|   };
 | |
| 
 | |
|   /*
 | |
|    * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps
 | |
|    * for the NALUs to the next stream component.
 | |
|    * Also, preprocess caption and sequence parameter NALUs.
 | |
|    *
 | |
|    * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push`
 | |
|    * @see NalByteStream.push
 | |
|    */
 | |
|   nalByteStream.on('data', function(data) {
 | |
|     var
 | |
|       event = {
 | |
|         trackId: trackId,
 | |
|         pts: currentPts,
 | |
|         dts: currentDts,
 | |
|         data: data,
 | |
|         nalUnitTypeCode: data[0] & 0x1f
 | |
|       };
 | |
| 
 | |
|     switch (event.nalUnitTypeCode) {
 | |
|     case 0x05:
 | |
|       event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr';
 | |
|       break;
 | |
|     case 0x06:
 | |
|       event.nalUnitType = 'sei_rbsp';
 | |
|       event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
 | |
|       break;
 | |
|     case 0x07:
 | |
|       event.nalUnitType = 'seq_parameter_set_rbsp';
 | |
|       event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
 | |
|       event.config = readSequenceParameterSet(event.escapedRBSP);
 | |
|       break;
 | |
|     case 0x08:
 | |
|       event.nalUnitType = 'pic_parameter_set_rbsp';
 | |
|       break;
 | |
|     case 0x09:
 | |
|       event.nalUnitType = 'access_unit_delimiter_rbsp';
 | |
|       break;
 | |
|     default:
 | |
|       break;
 | |
|     }
 | |
|     // This triggers data on the H264Stream
 | |
|     self.trigger('data', event);
 | |
|   });
 | |
|   nalByteStream.on('done', function() {
 | |
|     self.trigger('done');
 | |
|   });
 | |
|   nalByteStream.on('partialdone', function() {
 | |
|     self.trigger('partialdone');
 | |
|   });
 | |
|   nalByteStream.on('reset', function() {
 | |
|     self.trigger('reset');
 | |
|   });
 | |
|   nalByteStream.on('endedtimeline', function() {
 | |
|     self.trigger('endedtimeline');
 | |
|   });
 | |
| 
 | |
|   this.flush = function() {
 | |
|     nalByteStream.flush();
 | |
|   };
 | |
| 
 | |
|   this.partialFlush = function() {
 | |
|     nalByteStream.partialFlush();
 | |
|   };
 | |
| 
 | |
|   this.reset = function() {
 | |
|     nalByteStream.reset();
 | |
|   };
 | |
| 
 | |
|   this.endTimeline = function() {
 | |
|     nalByteStream.endTimeline();
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Advance the ExpGolomb decoder past a scaling list. The scaling
 | |
|    * list is optionally transmitted as part of a sequence parameter
 | |
|    * set and is not relevant to transmuxing.
 | |
|    * @param count {number} the number of entries in this scaling list
 | |
|    * @param expGolombDecoder {object} an ExpGolomb pointed to the
 | |
|    * start of a scaling list
 | |
|    * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
 | |
|    */
 | |
|   skipScalingList = function(count, expGolombDecoder) {
 | |
|     var
 | |
|       lastScale = 8,
 | |
|       nextScale = 8,
 | |
|       j,
 | |
|       deltaScale;
 | |
| 
 | |
|     for (j = 0; j < count; j++) {
 | |
|       if (nextScale !== 0) {
 | |
|         deltaScale = expGolombDecoder.readExpGolomb();
 | |
|         nextScale = (lastScale + deltaScale + 256) % 256;
 | |
|       }
 | |
| 
 | |
|       lastScale = (nextScale === 0) ? lastScale : nextScale;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Expunge any "Emulation Prevention" bytes from a "Raw Byte
 | |
|    * Sequence Payload"
 | |
|    * @param data {Uint8Array} the bytes of a RBSP from a NAL
 | |
|    * unit
 | |
|    * @return {Uint8Array} the RBSP without any Emulation
 | |
|    * Prevention Bytes
 | |
|    */
 | |
|   discardEmulationPreventionBytes = function(data) {
 | |
|     var
 | |
|       length = data.byteLength,
 | |
|       emulationPreventionBytesPositions = [],
 | |
|       i = 1,
 | |
|       newLength, newData;
 | |
| 
 | |
|     // Find all `Emulation Prevention Bytes`
 | |
|     while (i < length - 2) {
 | |
|       if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
 | |
|         emulationPreventionBytesPositions.push(i + 2);
 | |
|         i += 2;
 | |
|       } else {
 | |
|         i++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If no Emulation Prevention Bytes were found just return the original
 | |
|     // array
 | |
|     if (emulationPreventionBytesPositions.length === 0) {
 | |
|       return data;
 | |
|     }
 | |
| 
 | |
|     // Create a new array to hold the NAL unit data
 | |
|     newLength = length - emulationPreventionBytesPositions.length;
 | |
|     newData = new Uint8Array(newLength);
 | |
|     var sourceIndex = 0;
 | |
| 
 | |
|     for (i = 0; i < newLength; sourceIndex++, i++) {
 | |
|       if (sourceIndex === emulationPreventionBytesPositions[0]) {
 | |
|         // Skip this byte
 | |
|         sourceIndex++;
 | |
|         // Remove this position index
 | |
|         emulationPreventionBytesPositions.shift();
 | |
|       }
 | |
|       newData[i] = data[sourceIndex];
 | |
|     }
 | |
| 
 | |
|     return newData;
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Read a sequence parameter set and return some interesting video
 | |
|    * properties. A sequence parameter set is the H264 metadata that
 | |
|    * describes the properties of upcoming video frames.
 | |
|    * @param data {Uint8Array} the bytes of a sequence parameter set
 | |
|    * @return {object} an object with configuration parsed from the
 | |
|    * sequence parameter set, including the dimensions of the
 | |
|    * associated video frames.
 | |
|    */
 | |
|   readSequenceParameterSet = function(data) {
 | |
|     var
 | |
|       frameCropLeftOffset = 0,
 | |
|       frameCropRightOffset = 0,
 | |
|       frameCropTopOffset = 0,
 | |
|       frameCropBottomOffset = 0,
 | |
|       sarScale = 1,
 | |
|       expGolombDecoder, profileIdc, levelIdc, profileCompatibility,
 | |
|       chromaFormatIdc, picOrderCntType,
 | |
|       numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1,
 | |
|       picHeightInMapUnitsMinus1,
 | |
|       frameMbsOnlyFlag,
 | |
|       scalingListCount,
 | |
|       sarRatio = [1, 1],
 | |
|       aspectRatioIdc,
 | |
|       i;
 | |
| 
 | |
|     expGolombDecoder = new ExpGolomb(data);
 | |
|     profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
 | |
|     profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag
 | |
|     levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8)
 | |
|     expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
 | |
| 
 | |
|     // some profiles have more optional data we don't need
 | |
|     if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) {
 | |
|       chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
 | |
|       if (chromaFormatIdc === 3) {
 | |
|         expGolombDecoder.skipBits(1); // separate_colour_plane_flag
 | |
|       }
 | |
|       expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
 | |
|       expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
 | |
|       expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
 | |
|       if (expGolombDecoder.readBoolean()) { // seq_scaling_matrix_present_flag
 | |
|         scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12;
 | |
|         for (i = 0; i < scalingListCount; i++) {
 | |
|           if (expGolombDecoder.readBoolean()) { // seq_scaling_list_present_flag[ i ]
 | |
|             if (i < 6) {
 | |
|               skipScalingList(16, expGolombDecoder);
 | |
|             } else {
 | |
|               skipScalingList(64, expGolombDecoder);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
 | |
|     picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
 | |
| 
 | |
|     if (picOrderCntType === 0) {
 | |
|       expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4
 | |
|     } else if (picOrderCntType === 1) {
 | |
|       expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
 | |
|       expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
 | |
|       expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
 | |
|       numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
 | |
|       for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
 | |
|         expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
 | |
|     expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
 | |
| 
 | |
|     picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
 | |
|     picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
 | |
| 
 | |
|     frameMbsOnlyFlag = expGolombDecoder.readBits(1);
 | |
|     if (frameMbsOnlyFlag === 0) {
 | |
|       expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
 | |
|     }
 | |
| 
 | |
|     expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
 | |
|     if (expGolombDecoder.readBoolean()) { // frame_cropping_flag
 | |
|       frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
 | |
|       frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
 | |
|       frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
 | |
|       frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
 | |
|     }
 | |
|     if (expGolombDecoder.readBoolean()) {
 | |
|       // vui_parameters_present_flag
 | |
|       if (expGolombDecoder.readBoolean()) {
 | |
|         // aspect_ratio_info_present_flag
 | |
|         aspectRatioIdc = expGolombDecoder.readUnsignedByte();
 | |
|         switch (aspectRatioIdc) {
 | |
|           case 1: sarRatio = [1, 1]; break;
 | |
|           case 2: sarRatio = [12, 11]; break;
 | |
|           case 3: sarRatio = [10, 11]; break;
 | |
|           case 4: sarRatio = [16, 11]; break;
 | |
|           case 5: sarRatio = [40, 33]; break;
 | |
|           case 6: sarRatio = [24, 11]; break;
 | |
|           case 7: sarRatio = [20, 11]; break;
 | |
|           case 8: sarRatio = [32, 11]; break;
 | |
|           case 9: sarRatio = [80, 33]; break;
 | |
|           case 10: sarRatio = [18, 11]; break;
 | |
|           case 11: sarRatio = [15, 11]; break;
 | |
|           case 12: sarRatio = [64, 33]; break;
 | |
|           case 13: sarRatio = [160, 99]; break;
 | |
|           case 14: sarRatio = [4, 3]; break;
 | |
|           case 15: sarRatio = [3, 2]; break;
 | |
|           case 16: sarRatio = [2, 1]; break;
 | |
|           case 255: {
 | |
|             sarRatio = [expGolombDecoder.readUnsignedByte() << 8 |
 | |
|                         expGolombDecoder.readUnsignedByte(),
 | |
|                         expGolombDecoder.readUnsignedByte() << 8 |
 | |
|                         expGolombDecoder.readUnsignedByte() ];
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|         if (sarRatio) {
 | |
|           sarScale = sarRatio[0] / sarRatio[1];
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return {
 | |
|       profileIdc: profileIdc,
 | |
|       levelIdc: levelIdc,
 | |
|       profileCompatibility: profileCompatibility,
 | |
|       width: (((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2),
 | |
|       height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - (frameCropTopOffset * 2) - (frameCropBottomOffset * 2),
 | |
|       // sar is sample aspect ratio
 | |
|       sarRatio: sarRatio
 | |
|     };
 | |
|   };
 | |
| 
 | |
| };
 | |
| H264Stream.prototype = new Stream();
 | |
| 
 | |
| module.exports = {
 | |
|   H264Stream: H264Stream,
 | |
|   NalByteStream: NalByteStream
 | |
| };
 | 
