You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
14 KiB
486 lines
14 KiB
/**
|
|
* 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,
|
|
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
|
|
};
|
|
|
|
switch (data[0] & 0x1f) {
|
|
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,
|
|
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: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale),
|
|
height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - (frameCropTopOffset * 2) - (frameCropBottomOffset * 2),
|
|
sarRatio: sarRatio
|
|
};
|
|
};
|
|
|
|
};
|
|
H264Stream.prototype = new Stream();
|
|
|
|
module.exports = {
|
|
H264Stream: H264Stream,
|
|
NalByteStream: NalByteStream
|
|
};
|