diff --git a/dev/mux.html b/dev/mux.html
index f1e031e..c7bc618 100644
--- a/dev/mux.html
+++ b/dev/mux.html
@@ -44,6 +44,7 @@
format = new Metamuxer.Mp4OutputFormat({ fastStart: 'fragmented', minimumFragmentDuration: 2 });
format = new Metamuxer.MkvOutputFormat({ minimumClusterDuration: 2 });
format = new Metamuxer.WavOutputFormat();
+ format = new Metamuxer.MkvOutputFormat();
let target = new Metamuxer.BufferTarget();
/*
@@ -58,6 +59,7 @@
let output = new Metamuxer.Output({ format, target });
+ /*
let sourcy = new Metamuxer.AudioBufferSource({
codec: 'pcm-s16',
bitrate: Metamuxer.QUALITY_HIGH
@@ -81,6 +83,7 @@
await output.finalize();
await new Promise(() => {});
+ */
/*
let videoSource = new Metamuxer.EncodedVideoChunkSource('avc');
@@ -112,8 +115,8 @@
*/
let videoSource = new Metamuxer.CanvasSource(canvas, {
- codec: 'avc',
- fullCodecString: 'avc1.42001f',
+ codec: 'hevc',
+ //fullCodecString: 'avc1.42001f',
bitrate: 1e6
});
let audioSource = new Metamuxer.AudioBufferSource({
@@ -190,7 +193,7 @@ Testing... <00:17.350>One... <00:18.125>Two...
context.fillStyle = ['red', 'green', 'blue', 'yellow'][i % 4];
context.fillRect(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random());
- await videoSource.add(i / 10, 1 / 10, { keyFrame: true });
+ await videoSource.add(i / 10, 1 / 10);
}
let audioContext = new AudioContext();
@@ -203,5 +206,5 @@ Testing... <00:17.350>One... <00:18.125>Two...
await output.finalize();
console.log(target);
- //download(new Blob([target.buffer]), 'test' + format.fileExtension);
+ download(new Blob([target.buffer]), 'test' + format.fileExtension);
\ No newline at end of file
diff --git a/src/avc.ts b/src/avc.ts
index 28a9d1d..55da173 100644
--- a/src/avc.ts
+++ b/src/avc.ts
@@ -1,13 +1,14 @@
-import { assert, Bitstream, readExpGolomb } from './misc';
+import { assert, Bitstream, readExpGolomb, readSignedExpGolomb } from './misc';
// References:
// ISO 14496-15
-// ITU-T-REC-H.264
+// Rec. ITU-T H.264
+// Rec. ITU-T H.265
// https://stackoverflow.com/questions/24884827
/** Finds all NAL units in an AVC packet in Annex B format. */
const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
- const nalUnits: { type: number; data: Uint8Array }[] = [];
+ const nalUnits: Uint8Array[] = [];
let i = 0;
while (i < packetData.length) {
@@ -44,8 +45,7 @@ const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
if (i > 0 && startCodePos > i) {
const nalData = packetData.subarray(i, startCodePos);
if (nalData.length > 0) {
- const nalType = nalData[0]! & 0x1f;
- nalUnits.push({ type: nalType, data: nalData });
+ nalUnits.push(nalData);
}
}
@@ -56,14 +56,30 @@ const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
if (i < packetData.length) {
const nalData = packetData.subarray(i);
if (nalData.length > 0) {
- const nalType = nalData[0]! & 0x1f;
- nalUnits.push({ type: nalType, data: nalData });
+ nalUnits.push(nalData);
}
}
return nalUnits;
};
+const removeEmulationPreventionBytes = (data: Uint8Array) => {
+ const result: number[] = [];
+ const len = data.length;
+
+ for (let i = 0; i < len; i++) {
+ // Look for the 0x000003 pattern
+ if (i + 2 < len && data[i] === 0x00 && data[i + 1] === 0x00 && data[i + 2] === 0x03) {
+ result.push(0x00, 0x00); // Push the first two bytes
+ i += 2; // Skip the 0x03 byte
+ } else {
+ result.push(data[i]!);
+ }
+ }
+
+ return new Uint8Array(result);
+};
+
// Data specified in ISO 14496-15
export type AvcDecoderConfigurationRecord = {
configurationVersion: number;
@@ -71,23 +87,18 @@ export type AvcDecoderConfigurationRecord = {
profileCompatibility: number;
avcLevelIndication: number;
lengthSizeMinusOne: number;
- sequenceParameterSets: {
- sequenceParameterSetLength: number;
- sequenceParameterSetNalUnit: Uint8Array;
- }[];
- pictureParameterSets: {
- pictureParameterSetLength: number;
- pictureParameterSetNalUnit: Uint8Array;
- }[];
+ sequenceParameterSets: Uint8Array[];
+ pictureParameterSets: Uint8Array[];
// Fields only for specific profiles:
chromaFormat: number | null;
bitDepthLumaMinus8: number | null;
bitDepthChromaMinus8: number | null;
- sequenceParameterSetExt: {
- sequenceParameterSetExtLength: number;
- sequenceParameterSetExtNalUnit: Uint8Array;
- }[] | null;
+ sequenceParameterSetExt: Uint8Array[] | null;
+};
+
+const extractNalUnitTypeForAvc = (data: Uint8Array) => {
+ return data[0]! & 0x1F;
};
/** Builds an AvcDecoderConfigurationRecord from an AVC packet in Annex B format. */
@@ -95,9 +106,9 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
try {
const nalUnits = findNalUnitsInAnnexB(packetData);
- const spsUnits = nalUnits.filter(unit => unit.type === 7);
- const ppsUnits = nalUnits.filter(unit => unit.type === 8);
- const spsExtUnits = nalUnits.filter(unit => unit.type === 13);
+ const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 7);
+ const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 8);
+ const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 13);
if (spsUnits.length === 0) {
return null;
@@ -108,8 +119,8 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
}
// Let's get the first SPS for profile and level information
- const spsData = spsUnits[0]!.data;
- const bitstream = new Bitstream(spsData);
+ const spsData = spsUnits[0]!;
+ const bitstream = new Bitstream(removeEmulationPreventionBytes(spsData));
bitstream.skipBits(1); // forbidden_zero_bit
bitstream.skipBits(2); // nal_ref_idc
@@ -130,14 +141,8 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
profileCompatibility: constraint_flags,
avcLevelIndication: level_idc,
lengthSizeMinusOne: 3, // Typically 4 bytes for length field
- sequenceParameterSets: spsUnits.map(unit => ({
- sequenceParameterSetLength: unit.data.length,
- sequenceParameterSetNalUnit: unit.data,
- })),
- pictureParameterSets: ppsUnits.map(unit => ({
- pictureParameterSetLength: unit.data.length,
- pictureParameterSetNalUnit: unit.data,
- })),
+ sequenceParameterSets: spsUnits,
+ pictureParameterSets: ppsUnits,
chromaFormat: null,
bitDepthLumaMinus8: null,
bitDepthChromaMinus8: null,
@@ -165,11 +170,7 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
record.chromaFormat = chroma_format_idc;
record.bitDepthLumaMinus8 = bit_depth_luma_minus8;
record.bitDepthChromaMinus8 = bit_depth_chroma_minus8;
-
- record.sequenceParameterSetExt = spsExtUnits.map(unit => ({
- sequenceParameterSetExtLength: unit.data.length,
- sequenceParameterSetExtNalUnit: unit.data,
- }));
+ record.sequenceParameterSetExt = spsExtUnits;
}
return record;
@@ -195,11 +196,12 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
// Write SPS
for (const sps of record.sequenceParameterSets) {
- bytes.push(sps.sequenceParameterSetLength >> 8); // High byte
- bytes.push(sps.sequenceParameterSetLength & 0xFF); // Low byte
+ const length = sps.byteLength;
+ bytes.push(length >> 8); // High byte
+ bytes.push(length & 0xFF); // Low byte
- for (let i = 0; i < sps.sequenceParameterSetLength; i++) {
- bytes.push(sps.sequenceParameterSetNalUnit[i]!);
+ for (let i = 0; i < length; i++) {
+ bytes.push(sps[i]!);
}
}
@@ -207,11 +209,12 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
// Write PPS
for (const pps of record.pictureParameterSets) {
- bytes.push(pps.pictureParameterSetLength >> 8); // High byte
- bytes.push(pps.pictureParameterSetLength & 0xFF); // Low byte
+ const length = pps.byteLength;
+ bytes.push(length >> 8); // High byte
+ bytes.push(length & 0xFF); // Low byte
- for (let i = 0; i < pps.pictureParameterSetLength; i++) {
- bytes.push(pps.pictureParameterSetNalUnit[i]!);
+ for (let i = 0; i < length; i++) {
+ bytes.push(pps[i]!);
}
}
@@ -234,11 +237,12 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
// Write SPS Ext
for (const spsExt of record.sequenceParameterSetExt) {
- bytes.push(spsExt.sequenceParameterSetExtLength >> 8); // High byte
- bytes.push(spsExt.sequenceParameterSetExtLength & 0xFF); // Low byte
+ const length = spsExt.byteLength;
+ bytes.push(length >> 8); // High byte
+ bytes.push(length & 0xFF); // Low byte
- for (let i = 0; i < spsExt.sequenceParameterSetExtLength; i++) {
- bytes.push(spsExt.sequenceParameterSetExtNalUnit[i]!);
+ for (let i = 0; i < length; i++) {
+ bytes.push(spsExt[i]!);
}
}
}
@@ -246,8 +250,8 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
return new Uint8Array(bytes);
};
-/** Converts an AVC packet in Annex B format to AVCC format. */
-export const transformAnnexBToAvcc = (packetData: Uint8Array) => {
+/** Converts an AVC packet in Annex B format to length-prefixed format. */
+export const transformAnnexBToLengthPrefixed = (packetData: Uint8Array) => {
const NAL_UNIT_LENGTH_SIZE = 4;
const nalUnits = findNalUnitsInAnnexB(packetData);
@@ -259,7 +263,7 @@ export const transformAnnexBToAvcc = (packetData: Uint8Array) => {
let totalSize = 0;
for (const nalUnit of nalUnits) {
- totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.data.length;
+ totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.byteLength;
}
const avccData = new Uint8Array(totalSize);
@@ -268,14 +272,579 @@ export const transformAnnexBToAvcc = (packetData: Uint8Array) => {
// Write each NAL unit with its length prefix
for (const nalUnit of nalUnits) {
- const length = nalUnit.data.length;
+ const length = nalUnit.byteLength;
dataView.setUint32(offset, length, false);
offset += 4;
- avccData.set(nalUnit.data, offset);
- offset += nalUnit.data.length;
+ avccData.set(nalUnit, offset);
+ offset += nalUnit.byteLength;
}
return avccData;
};
+
+const NALU_TYPE_VPS = 32;
+const NALU_TYPE_SPS = 33;
+const NALU_TYPE_PPS = 34;
+const NALU_TYPE_SEI_PREFIX = 39;
+const NALU_TYPE_SEI_SUFFIX = 40;
+
+export type HevcDecoderConfigurationRecord = {
+ configurationVersion: number;
+ generalProfileSpace: number;
+ generalTierFlag: number;
+ generalProfileIdc: number;
+ generalProfileCompatibilityFlags: number;
+ generalConstraintIndicatorFlags: Uint8Array; // 6 bytes long
+ generalLevelIdc: number;
+ minSpatialSegmentationIdc: number;
+ parallelismType: number;
+ chromaFormatIdc: number;
+ bitDepthLumaMinus8: number;
+ bitDepthChromaMinus8: number;
+ avgFrameRate: number;
+ constantFrameRate: number;
+ numTemporalLayers: number;
+ temporalIdNested: number;
+ lengthSizeMinusOne: number;
+ arrays: {
+ arrayCompleteness: number;
+ nalUnitType: number;
+ nalUnits: Uint8Array[];
+ }[];
+};
+
+const extractNalUnitTypeForHevc = (data: Uint8Array) => {
+ return (data[0]! >> 1) & 0x3F;
+};
+
+export const extractHevcDecoderConfigurationRecord = (
+ packetData: Uint8Array,
+) => {
+ try {
+ const nalUnits = findNalUnitsInAnnexB(packetData);
+
+ const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_VPS);
+ const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SPS);
+ const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_PPS);
+ const seiUnits = nalUnits.filter(
+ unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_PREFIX
+ || extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_SUFFIX,
+ );
+
+ if (spsUnits.length === 0 || ppsUnits.length === 0) return null;
+
+ const sps = spsUnits[0]!;
+ const bitstream = new Bitstream(removeEmulationPreventionBytes(sps));
+
+ bitstream.skipBits(16); // NAL header
+
+ bitstream.readBits(4); // sps_video_parameter_set_id
+ const sps_max_sub_layers_minus1 = bitstream.readBits(3);
+ const sps_temporal_id_nesting_flag = bitstream.readBits(1);
+
+ const {
+ general_profile_space,
+ general_tier_flag,
+ general_profile_idc,
+ general_profile_compatibility_flags,
+ general_constraint_indicator_flags,
+ general_level_idc,
+ } = parseProfileTierLevel(bitstream, sps_max_sub_layers_minus1);
+
+ readExpGolomb(bitstream); // sps_seq_parameter_set_id
+
+ const chroma_format_idc = readExpGolomb(bitstream);
+ if (chroma_format_idc === 3) bitstream.skipBits(1); // separate_colour_plane_flag
+
+ readExpGolomb(bitstream); // pic_width_in_luma_samples
+ readExpGolomb(bitstream); // pic_height_in_luma_samples
+
+ if (bitstream.readBits(1)) { // conformance_window_flag
+ readExpGolomb(bitstream); // conf_win_left_offset
+ readExpGolomb(bitstream); // conf_win_right_offset
+ readExpGolomb(bitstream); // conf_win_top_offset
+ readExpGolomb(bitstream); // conf_win_bottom_offset
+ }
+
+ const bit_depth_luma_minus8 = readExpGolomb(bitstream);
+ const bit_depth_chroma_minus8 = readExpGolomb(bitstream);
+
+ readExpGolomb(bitstream); // log2_max_pic_order_cnt_lsb_minus4
+
+ const sps_sub_layer_ordering_info_present_flag = bitstream.readBits(1);
+ const maxNum = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1;
+ for (let i = maxNum; i <= sps_max_sub_layers_minus1; i++) {
+ readExpGolomb(bitstream); // sps_max_dec_pic_buffering_minus1[i]
+ readExpGolomb(bitstream); // sps_max_num_reorder_pics[i]
+ readExpGolomb(bitstream); // sps_max_latency_increase_plus1[i]
+ }
+
+ readExpGolomb(bitstream); // log2_min_luma_coding_block_size_minus3
+ readExpGolomb(bitstream); // log2_diff_max_min_luma_coding_block_size
+ readExpGolomb(bitstream); // log2_min_luma_transform_block_size_minus2
+ readExpGolomb(bitstream); // log2_diff_max_min_luma_transform_block_size
+ readExpGolomb(bitstream); // max_transform_hierarchy_depth_inter
+ readExpGolomb(bitstream); // max_transform_hierarchy_depth_intra
+
+ if (bitstream.readBits(1)) { // scaling_list_enabled_flag
+ if (bitstream.readBits(1)) {
+ skipScalingListData(bitstream);
+ }
+ }
+
+ bitstream.skipBits(1); // amp_enabled_flag
+ bitstream.skipBits(1); // sample_adaptive_offset_enabled_flag
+
+ if (bitstream.readBits(1)) { // pcm_enabled_flag
+ bitstream.skipBits(4); // pcm_sample_bit_depth_luma_minus1
+ bitstream.skipBits(4); // pcm_sample_bit_depth_chroma_minus1
+ readExpGolomb(bitstream); // log2_min_pcm_luma_coding_block_size_minus3
+ readExpGolomb(bitstream); // log2_diff_max_min_pcm_luma_coding_block_size
+ bitstream.skipBits(1); // pcm_loop_filter_disabled_flag
+ }
+
+ const num_short_term_ref_pic_sets = readExpGolomb(bitstream);
+ skipAllStRefPicSets(bitstream, num_short_term_ref_pic_sets);
+
+ if (bitstream.readBits(1)) { // long_term_ref_pics_present_flag
+ const num_long_term_ref_pics_sps = readExpGolomb(bitstream);
+ for (let i = 0; i < num_long_term_ref_pics_sps; i++) {
+ readExpGolomb(bitstream); // lt_ref_pic_poc_lsb_sps[i]
+ bitstream.skipBits(1); // used_by_curr_pic_lt_sps_flag[i]
+ }
+ }
+
+ bitstream.skipBits(1); // sps_temporal_mvp_enabled_flag
+ bitstream.skipBits(1); // strong_intra_smoothing_enabled_flag
+
+ let min_spatial_segmentation_idc = 0;
+ if (bitstream.readBits(1)) { // vui_parameters_present_flag
+ min_spatial_segmentation_idc = parseVuiForMinSpatialSegmentationIdc(bitstream, sps_max_sub_layers_minus1);
+ }
+
+ // Parse PPS for parallelismType
+ let parallelismType = 0;
+ if (ppsUnits.length > 0) {
+ const pps = ppsUnits[0]!;
+ const ppsBitstream = new Bitstream(removeEmulationPreventionBytes(pps));
+
+ ppsBitstream.skipBits(16); // NAL header
+ readExpGolomb(ppsBitstream); // pps_pic_parameter_set_id
+ readExpGolomb(ppsBitstream); // pps_seq_parameter_set_id
+ ppsBitstream.skipBits(1); // dependent_slice_segments_enabled_flag
+ ppsBitstream.skipBits(1); // output_flag_present_flag
+ ppsBitstream.skipBits(3); // num_extra_slice_header_bits
+ ppsBitstream.skipBits(1); // sign_data_hiding_enabled_flag
+ ppsBitstream.skipBits(1); // cabac_init_present_flag
+ readExpGolomb(ppsBitstream); // num_ref_idx_l0_default_active_minus1
+ readExpGolomb(ppsBitstream); // num_ref_idx_l1_default_active_minus1
+ readSignedExpGolomb(ppsBitstream); // init_qp_minus26
+ ppsBitstream.skipBits(1); // constrained_intra_pred_flag
+ ppsBitstream.skipBits(1); // transform_skip_enabled_flag
+ if (ppsBitstream.readBits(1)) { // cu_qp_delta_enabled_flag
+ readExpGolomb(ppsBitstream); // diff_cu_qp_delta_depth
+ }
+ readSignedExpGolomb(ppsBitstream); // pps_cb_qp_offset
+ readSignedExpGolomb(ppsBitstream); // pps_cr_qp_offset
+ ppsBitstream.skipBits(1); // pps_slice_chroma_qp_offsets_present_flag
+ ppsBitstream.skipBits(1); // weighted_pred_flag
+ ppsBitstream.skipBits(1); // weighted_bipred_flag
+ ppsBitstream.skipBits(1); // transquant_bypass_enabled_flag
+ const tiles_enabled_flag = ppsBitstream.readBits(1);
+ const entropy_coding_sync_enabled_flag = ppsBitstream.readBits(1);
+
+ if (!tiles_enabled_flag && !entropy_coding_sync_enabled_flag) parallelismType = 0;
+ else if (tiles_enabled_flag && !entropy_coding_sync_enabled_flag) parallelismType = 2;
+ else if (!tiles_enabled_flag && entropy_coding_sync_enabled_flag) parallelismType = 3;
+ else parallelismType = 0;
+ }
+
+ const arrays = [
+ ...(vpsUnits.length
+ ? [
+ {
+ arrayCompleteness: 1,
+ nalUnitType: NALU_TYPE_VPS,
+ nalUnits: vpsUnits,
+ },
+ ]
+ : []),
+ ...(spsUnits.length
+ ? [
+ {
+ arrayCompleteness: 1,
+ nalUnitType: NALU_TYPE_SPS,
+ nalUnits: spsUnits,
+ },
+ ]
+ : []),
+ ...(ppsUnits.length
+ ? [
+ {
+ arrayCompleteness: 1,
+ nalUnitType: NALU_TYPE_PPS,
+ nalUnits: ppsUnits,
+ },
+ ]
+ : []),
+ ...(seiUnits.length
+ ? [
+ {
+ arrayCompleteness: 1,
+ nalUnitType: extractNalUnitTypeForHevc(seiUnits[0]!),
+ nalUnits: seiUnits,
+ },
+ ]
+ : []),
+ ];
+
+ const record: HevcDecoderConfigurationRecord = {
+ configurationVersion: 1,
+ generalProfileSpace: general_profile_space,
+ generalTierFlag: general_tier_flag,
+ generalProfileIdc: general_profile_idc,
+ generalProfileCompatibilityFlags: general_profile_compatibility_flags,
+ generalConstraintIndicatorFlags: general_constraint_indicator_flags,
+ generalLevelIdc: general_level_idc,
+ minSpatialSegmentationIdc: min_spatial_segmentation_idc,
+ parallelismType,
+ chromaFormatIdc: chroma_format_idc,
+ bitDepthLumaMinus8: bit_depth_luma_minus8,
+ bitDepthChromaMinus8: bit_depth_chroma_minus8,
+ avgFrameRate: 0,
+ constantFrameRate: 0,
+ numTemporalLayers: sps_max_sub_layers_minus1 + 1,
+ temporalIdNested: sps_temporal_id_nesting_flag,
+ lengthSizeMinusOne: 3,
+ arrays,
+ };
+
+ return record;
+ } catch (error) {
+ console.error('Error building HEVC Decoder Configuration Record:', error);
+ return null;
+ }
+};
+
+const parseProfileTierLevel = (
+ bitstream: Bitstream,
+ maxNumSubLayersMinus1: number,
+) => {
+ const general_profile_space = bitstream.readBits(2);
+ const general_tier_flag = bitstream.readBits(1);
+ const general_profile_idc = bitstream.readBits(5);
+
+ let general_profile_compatibility_flags = 0;
+ for (let i = 0; i < 32; i++) {
+ general_profile_compatibility_flags = (general_profile_compatibility_flags << 1) | bitstream.readBits(1);
+ }
+
+ const general_constraint_indicator_flags = new Uint8Array(6);
+ for (let i = 0; i < 6; i++) {
+ general_constraint_indicator_flags[i] = bitstream.readBits(8);
+ }
+
+ const general_level_idc = bitstream.readBits(8);
+
+ const sub_layer_profile_present_flag: number[] = [];
+ const sub_layer_level_present_flag: number[] = [];
+ for (let i = 0; i < maxNumSubLayersMinus1; i++) {
+ sub_layer_profile_present_flag.push(bitstream.readBits(1));
+ sub_layer_level_present_flag.push(bitstream.readBits(1));
+ }
+ if (maxNumSubLayersMinus1 > 0) {
+ for (let i = maxNumSubLayersMinus1; i < 8; i++) {
+ bitstream.skipBits(2); // reserved_zero_2bits
+ }
+ }
+ for (let i = 0; i < maxNumSubLayersMinus1; i++) {
+ if (sub_layer_profile_present_flag[i]) bitstream.skipBits(88);
+ if (sub_layer_level_present_flag[i]) bitstream.skipBits(8);
+ }
+
+ return {
+ general_profile_space,
+ general_tier_flag,
+ general_profile_idc,
+ general_profile_compatibility_flags,
+ general_constraint_indicator_flags,
+ general_level_idc,
+ };
+};
+
+const skipScalingListData = (bitstream: Bitstream) => {
+ for (let sizeId = 0; sizeId < 4; sizeId++) {
+ for (let matrixId = 0; matrixId < (sizeId === 3 ? 2 : 6); matrixId++) {
+ const scaling_list_pred_mode_flag = bitstream.readBits(1);
+ if (!scaling_list_pred_mode_flag) {
+ readExpGolomb(bitstream); // scaling_list_pred_matrix_id_delta
+ } else {
+ const coefNum = Math.min(64, 1 << (4 + (sizeId << 1)));
+ if (sizeId > 1) {
+ readSignedExpGolomb(bitstream); // scaling_list_dc_coef_minus8
+ }
+ for (let i = 0; i < coefNum; i++) {
+ readSignedExpGolomb(bitstream); // scaling_list_delta_coef
+ }
+ }
+ }
+ }
+};
+
+const skipAllStRefPicSets = (bitstream: Bitstream, num_short_term_ref_pic_sets: number) => {
+ const NumDeltaPocs: number[] = [];
+ for (let stRpsIdx = 0; stRpsIdx < num_short_term_ref_pic_sets; stRpsIdx++) {
+ NumDeltaPocs[stRpsIdx] = skipStRefPicSet(bitstream, stRpsIdx, num_short_term_ref_pic_sets, NumDeltaPocs);
+ }
+};
+
+const skipStRefPicSet = (
+ bitstream: Bitstream,
+ stRpsIdx: number,
+ num_short_term_ref_pic_sets: number,
+ NumDeltaPocs: number[],
+) => {
+ let NumDeltaPocsThis = 0;
+ let inter_ref_pic_set_prediction_flag = 0;
+ let RefRpsIdx = 0;
+
+ if (stRpsIdx !== 0) {
+ inter_ref_pic_set_prediction_flag = bitstream.readBits(1);
+ }
+ if (inter_ref_pic_set_prediction_flag) {
+ if (stRpsIdx === num_short_term_ref_pic_sets) {
+ const delta_idx_minus1 = readExpGolomb(bitstream);
+ RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1);
+ } else {
+ RefRpsIdx = stRpsIdx - 1;
+ }
+ bitstream.readBits(1); // delta_rps_sign
+ readExpGolomb(bitstream); // abs_delta_rps_minus1
+
+ // The number of iterations is NumDeltaPocs[RefRpsIdx] + 1
+ const numDelta = NumDeltaPocs[RefRpsIdx] ?? 0;
+ for (let j = 0; j <= numDelta; j++) {
+ const used_by_curr_pic_flag = bitstream.readBits(1);
+ if (!used_by_curr_pic_flag) {
+ bitstream.readBits(1); // use_delta_flag
+ }
+ }
+ NumDeltaPocsThis = NumDeltaPocs[RefRpsIdx]!;
+ } else {
+ const num_negative_pics = readExpGolomb(bitstream);
+ const num_positive_pics = readExpGolomb(bitstream);
+
+ for (let i = 0; i < num_negative_pics; i++) {
+ readExpGolomb(bitstream); // delta_poc_s0_minus1[i]
+ bitstream.readBits(1); // used_by_curr_pic_s0_flag[i]
+ }
+ for (let i = 0; i < num_positive_pics; i++) {
+ readExpGolomb(bitstream); // delta_poc_s1_minus1[i]
+ bitstream.readBits(1); // used_by_curr_pic_s1_flag[i]
+ }
+ NumDeltaPocsThis = num_negative_pics + num_positive_pics;
+ }
+ return NumDeltaPocsThis;
+};
+
+const parseVuiForMinSpatialSegmentationIdc = (bitstream: Bitstream, sps_max_sub_layers_minus1: number) => {
+ if (bitstream.readBits(1)) { // aspect_ratio_info_present_flag
+ const aspect_ratio_idc = bitstream.readBits(8);
+ if (aspect_ratio_idc === 255) {
+ bitstream.readBits(16); // sar_width
+ bitstream.readBits(16); // sar_height
+ }
+ }
+ if (bitstream.readBits(1)) { // overscan_info_present_flag
+ bitstream.readBits(1); // overscan_appropriate_flag
+ }
+ if (bitstream.readBits(1)) { // video_signal_type_present_flag
+ bitstream.readBits(3); // video_format
+ bitstream.readBits(1); // video_full_range_flag
+ if (bitstream.readBits(1)) {
+ bitstream.readBits(8); // colour_primaries
+ bitstream.readBits(8); // transfer_characteristics
+ bitstream.readBits(8); // matrix_coeffs
+ }
+ }
+ if (bitstream.readBits(1)) { // chroma_loc_info_present_flag
+ readExpGolomb(bitstream); // chroma_sample_loc_type_top_field
+ readExpGolomb(bitstream); // chroma_sample_loc_type_bottom_field
+ }
+ bitstream.readBits(1); // neutral_chroma_indication_flag
+ bitstream.readBits(1); // field_seq_flag
+ bitstream.readBits(1); // frame_field_info_present_flag
+ if (bitstream.readBits(1)) { // default_display_window_flag
+ readExpGolomb(bitstream); // def_disp_win_left_offset
+ readExpGolomb(bitstream); // def_disp_win_right_offset
+ readExpGolomb(bitstream); // def_disp_win_top_offset
+ readExpGolomb(bitstream); // def_disp_win_bottom_offset
+ }
+ if (bitstream.readBits(1)) { // vui_timing_info_present_flag
+ bitstream.readBits(32); // vui_num_units_in_tick
+ bitstream.readBits(32); // vui_time_scale
+ if (bitstream.readBits(1)) { // vui_poc_proportional_to_timing_flag
+ readExpGolomb(bitstream); // vui_num_ticks_poc_diff_one_minus1
+ }
+ if (bitstream.readBits(1)) {
+ skipHrdParameters(bitstream, true, sps_max_sub_layers_minus1);
+ }
+ }
+ if (bitstream.readBits(1)) { // bitstream_restriction_flag
+ bitstream.readBits(1); // tiles_fixed_structure_flag
+ bitstream.readBits(1); // motion_vectors_over_pic_boundaries_flag
+ bitstream.readBits(1); // restricted_ref_pic_lists_flag
+ const min_spatial_segmentation_idc = readExpGolomb(bitstream);
+ // skip the rest
+ readExpGolomb(bitstream); // max_bytes_per_pic_denom
+ readExpGolomb(bitstream); // max_bits_per_min_cu_denom
+ readExpGolomb(bitstream); // log2_max_mv_length_horizontal
+ readExpGolomb(bitstream); // log2_max_mv_length_vertical
+ return min_spatial_segmentation_idc;
+ }
+ return 0;
+};
+
+const skipHrdParameters = (
+ bitstream: Bitstream,
+ commonInfPresentFlag: boolean,
+ maxNumSubLayersMinus1: number,
+) => {
+ let nal_hrd_parameters_present_flag = false;
+ let vcl_hrd_parameters_present_flag = false;
+ let sub_pic_hrd_params_present_flag = false;
+
+ if (commonInfPresentFlag) {
+ nal_hrd_parameters_present_flag = bitstream.readBits(1) === 1;
+ vcl_hrd_parameters_present_flag = bitstream.readBits(1) === 1;
+ if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) {
+ sub_pic_hrd_params_present_flag = bitstream.readBits(1) === 1;
+ if (sub_pic_hrd_params_present_flag) {
+ bitstream.readBits(8); // tick_divisor_minus2
+ bitstream.readBits(5); // du_cpb_removal_delay_increment_length_minus1
+ bitstream.readBits(1); // sub_pic_cpb_params_in_pic_timing_sei_flag
+ bitstream.readBits(5); // dpb_output_delay_du_length_minus1
+ }
+ bitstream.readBits(4); // bit_rate_scale
+ bitstream.readBits(4); // cpb_size_scale
+ if (sub_pic_hrd_params_present_flag) {
+ bitstream.readBits(4); // cpb_size_du_scale
+ }
+ bitstream.readBits(5); // initial_cpb_removal_delay_length_minus1
+ bitstream.readBits(5); // au_cpb_removal_delay_length_minus1
+ bitstream.readBits(5); // dpb_output_delay_length_minus1
+ }
+ }
+
+ for (let i = 0; i <= maxNumSubLayersMinus1; i++) {
+ const fixed_pic_rate_general_flag = bitstream.readBits(1) === 1;
+ let fixed_pic_rate_within_cvs_flag = true; // Default assumption if general is true
+ if (!fixed_pic_rate_general_flag) {
+ fixed_pic_rate_within_cvs_flag = bitstream.readBits(1) === 1;
+ }
+
+ let low_delay_hrd_flag = false; // Default assumption
+ if (fixed_pic_rate_within_cvs_flag) {
+ readExpGolomb(bitstream); // elemental_duration_in_tc_minus1[i]
+ } else {
+ low_delay_hrd_flag = bitstream.readBits(1) === 1;
+ }
+
+ let CpbCnt = 1; // Default if low_delay is true
+ if (!low_delay_hrd_flag) {
+ const cpb_cnt_minus1 = readExpGolomb(bitstream); // cpb_cnt_minus1[i]
+ CpbCnt = cpb_cnt_minus1 + 1;
+ }
+
+ if (nal_hrd_parameters_present_flag) {
+ skipSubLayerHrdParameters(bitstream, CpbCnt, sub_pic_hrd_params_present_flag);
+ }
+ if (vcl_hrd_parameters_present_flag) {
+ skipSubLayerHrdParameters(bitstream, CpbCnt, sub_pic_hrd_params_present_flag);
+ }
+ }
+};
+
+const skipSubLayerHrdParameters = (
+ bitstream: Bitstream,
+ CpbCnt: number,
+ sub_pic_hrd_params_present_flag: boolean,
+) => {
+ for (let i = 0; i < CpbCnt; i++) {
+ readExpGolomb(bitstream); // bit_rate_value_minus1[i]
+ readExpGolomb(bitstream); // cpb_size_value_minus1[i]
+ if (sub_pic_hrd_params_present_flag) {
+ readExpGolomb(bitstream); // cpb_size_du_value_minus1[i]
+ readExpGolomb(bitstream); // bit_rate_du_value_minus1[i]
+ }
+ bitstream.readBits(1); // cbr_flag[i]
+ }
+};
+
+export const serializeHevcDecoderConfigurationRecord = (record: HevcDecoderConfigurationRecord) => {
+ const bytes: number[] = [];
+
+ bytes.push(record.configurationVersion);
+
+ bytes.push(
+ ((record.generalProfileSpace & 0x3) << 6)
+ | ((record.generalTierFlag & 0x1) << 5)
+ | (record.generalProfileIdc & 0x1F),
+ );
+
+ bytes.push((record.generalProfileCompatibilityFlags >>> 24) & 0xFF);
+ bytes.push((record.generalProfileCompatibilityFlags >>> 16) & 0xFF);
+ bytes.push((record.generalProfileCompatibilityFlags >>> 8) & 0xFF);
+ bytes.push(record.generalProfileCompatibilityFlags & 0xFF);
+
+ bytes.push(...record.generalConstraintIndicatorFlags);
+
+ bytes.push(record.generalLevelIdc & 0xFF);
+
+ bytes.push(0xF0 | ((record.minSpatialSegmentationIdc >> 8) & 0x0F)); // Reserved + high nibble
+ bytes.push(record.minSpatialSegmentationIdc & 0xFF); // Low byte
+
+ bytes.push(0xFC | (record.parallelismType & 0x03));
+
+ bytes.push(0xFC | (record.chromaFormatIdc & 0x03));
+
+ bytes.push(0xF8 | (record.bitDepthLumaMinus8 & 0x07));
+
+ bytes.push(0xF8 | (record.bitDepthChromaMinus8 & 0x07));
+
+ bytes.push((record.avgFrameRate >> 8) & 0xFF); // High byte
+ bytes.push(record.avgFrameRate & 0xFF); // Low byte
+
+ bytes.push(
+ ((record.constantFrameRate & 0x03) << 6)
+ | ((record.numTemporalLayers & 0x07) << 3)
+ | ((record.temporalIdNested & 0x01) << 2)
+ | (record.lengthSizeMinusOne & 0x03),
+ );
+
+ bytes.push(record.arrays.length & 0xFF);
+
+ for (const arr of record.arrays) {
+ bytes.push(
+ ((arr.arrayCompleteness & 0x01) << 7)
+ | (0 << 6)
+ | (arr.nalUnitType & 0x3F),
+ );
+
+ bytes.push((arr.nalUnits.length >> 8) & 0xFF); // High byte
+ bytes.push(arr.nalUnits.length & 0xFF); // Low byte
+
+ for (const nal of arr.nalUnits) {
+ bytes.push((nal.length >> 8) & 0xFF); // High byte
+ bytes.push(nal.length & 0xFF); // Low byte
+
+ for (let i = 0; i < nal.length; i++) {
+ bytes.push(nal[i]!);
+ }
+ }
+ }
+
+ return new Uint8Array(bytes);
+};
diff --git a/src/codec.ts b/src/codec.ts
index 8414b69..e8267fa 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -1,4 +1,4 @@
-import { AvcDecoderConfigurationRecord } from './avc';
+import { AvcDecoderConfigurationRecord, HevcDecoderConfigurationRecord } from './avc';
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
import {
Bitstream,
@@ -337,10 +337,11 @@ export const extractVideoCodecString = (trackInfo: {
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
+ hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
}) => {
- const { codec, codecDescription: description, colorSpace, avcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
+ const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
if (codec === 'avc') {
if (avcCodecInfo) {
@@ -353,52 +354,63 @@ export const extractVideoCodecString = (trackInfo: {
return `avc1.${bytesToHexString(bytes)}`;
}
- if (!description || description.byteLength < 4) {
+ if (!codecDescription || codecDescription.byteLength < 4) {
throw new TypeError('AVC decoder description is not provided or is not at least 4 bytes long.');
}
- return `avc1.${bytesToHexString(description.subarray(1, 4))}`;
+ return `avc1.${bytesToHexString(codecDescription.subarray(1, 4))}`;
} else if (codec === 'hevc') {
- if (!description) {
- throw new TypeError('HEVC decoder description is not provided.');
+ let generalProfileSpace: number;
+ let generalProfileIdc: number;
+ let compatibilityFlags: number;
+ let generalTierFlag: number;
+ let generalLevelIdc: number;
+ let constraintFlags: number[];
+
+ if (hevcCodecInfo) {
+ generalProfileSpace = hevcCodecInfo.generalProfileSpace;
+ generalProfileIdc = hevcCodecInfo.generalProfileIdc;
+ compatibilityFlags = reverseBitsU32(hevcCodecInfo.generalProfileCompatibilityFlags);
+ generalTierFlag = hevcCodecInfo.generalTierFlag;
+ generalLevelIdc = hevcCodecInfo.generalLevelIdc;
+ constraintFlags = [...hevcCodecInfo.generalConstraintIndicatorFlags];
+ } else {
+ if (!codecDescription || codecDescription.byteLength < 23) {
+ throw new TypeError('HEVC decoder description is not provided or is not at least 23 bytes long.');
+ }
+
+ const view = toDataView(codecDescription);
+ const profileByte = view.getUint8(1);
+
+ generalProfileSpace = (profileByte >> 6) & 0x03;
+ generalProfileIdc = profileByte & 0x1F;
+ compatibilityFlags = reverseBitsU32(view.getUint32(2));
+ generalTierFlag = (profileByte >> 5) & 0x01;
+ generalLevelIdc = view.getUint8(12);
+
+ constraintFlags = [];
+ for (let i = 0; i < 6; i++) {
+ constraintFlags.push(view.getUint8(6 + i));
+ }
}
- const view = toDataView(description);
let codecString = 'hev1.';
- // general_profile_space and general_profile_idc
- const generalProfileSpace = (description[1]! >> 6) & 0x03;
- const generalProfileIdc = description[1]! & 0x1F;
codecString += ['', 'A', 'B', 'C'][generalProfileSpace]! + generalProfileIdc;
-
codecString += '.';
-
- // general_profile_compatibility_flags (in reverse bit order)
- const compatibilityFlags = reverseBitsU32(view.getUint32(2));
- codecString += compatibilityFlags.toString(16);
-
+ codecString += compatibilityFlags.toString(16).toUpperCase();
codecString += '.';
-
- // general_tier_flag and general_level_idc
- const generalTierFlag = (description[1]! >> 5) & 0x01;
- const generalLevelIdc = description[12]!;
codecString += generalTierFlag === 0 ? 'L' : 'H';
codecString += generalLevelIdc;
- codecString += '.';
-
- // constraint_flags (6 bytes)
- const constraintFlags: number[] = [];
- for (let i = 0; i < 6; i++) {
- const byte = description[i + 13]!;
- constraintFlags.push(byte);
- }
-
- while (constraintFlags[constraintFlags.length - 1] === 0) {
+ while (constraintFlags.length > 0 && constraintFlags[constraintFlags.length - 1] === 0) {
constraintFlags.pop();
}
- codecString += constraintFlags.map(x => x.toString(16)).join('.');
+ if (constraintFlags.length > 0) {
+ codecString += '.';
+ codecString += constraintFlags.map(x => x.toString(16).toUpperCase()).join('.');
+ }
return codecString;
} else if (codec === 'vp8') {
@@ -1367,12 +1379,9 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
);
}
- if (!metadata.decoderConfig.description) {
- throw new TypeError(
- 'Video chunk metadata decoder configuration for HEVC must include a description, which is expected to'
- + ' be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.',
- );
- }
+ // `description` may or may not be set, depending on if the format is HEVC or Annex B, so don't perform any
+ // validation for it.
+ // https://www.w3.org/TR/webcodecs-hevc-codec-registration
} else if (metadata.decoderConfig.codec.startsWith('vp8')) {
// VP8-specific validation
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index b961eed..6dc10db 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -1,4 +1,4 @@
-import { AvcDecoderConfigurationRecord } from '../avc';
+import { AvcDecoderConfigurationRecord, HevcDecoderConfigurationRecord } from '../avc';
import {
AacCodecInfo,
AudioCodec,
@@ -75,6 +75,7 @@ type InternalTrack = {
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
+ hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
};
@@ -762,6 +763,7 @@ export class IsobmffDemuxer extends Demuxer {
codecDescription: null,
colorSpace: null,
avcCodecInfo: null,
+ hevcCodecInfo: null,
vp9CodecInfo: null,
av1CodecInfo: null,
};
diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts
index 5a3c9a7..7d2e4a1 100644
--- a/src/isobmff/isobmff-muxer.ts
+++ b/src/isobmff/isobmff-muxer.ts
@@ -17,8 +17,10 @@ import { BufferTarget } from '../target';
import { EncodedPacket, PacketType } from '../packet';
import {
extractAvcDecoderConfigurationRecord,
+ extractHevcDecoderConfigurationRecord,
serializeAvcDecoderConfigurationRecord,
- transformAnnexBToAvcc,
+ serializeHevcDecoderConfigurationRecord,
+ transformAnnexBToLengthPrefixed,
} from '../avc';
export const GLOBAL_TIMESCALE = 1000;
@@ -67,10 +69,11 @@ export type IsobmffTrackData = {
height: number;
decoderConfig: VideoDecoderConfig;
/**
- * The "AVC transformation" involves converting the raw AVC packet data from Annex B to AVCC format.
+ * The "Annex B transformation" involves converting the raw packet data from Annex B to
+ * "MP4" (length-prefixed) format.
* https://stackoverflow.com/questions/24884827
*/
- requiresAvcTransformation: boolean;
+ requiresAnnexBTransformation: boolean;
};
} | {
track: OutputAudioTrack;
@@ -208,7 +211,7 @@ export class IsobmffMuxer extends Muxer {
assert(decoderConfig.codedWidth !== undefined);
assert(decoderConfig.codedHeight !== undefined);
- let requiresAvcTransformation = false;
+ let requiresAnnexBTransformation = false;
if (track.source._codec === 'avc' && !decoderConfig.description) {
// ISOBMFF can only hold AVC in the AVCC format, not in Annex B, but the missing description indicates
@@ -225,7 +228,23 @@ export class IsobmffMuxer extends Muxer {
}
decoderConfig.description = serializeAvcDecoderConfigurationRecord(decoderConfigurationRecord);
- requiresAvcTransformation = true;
+ requiresAnnexBTransformation = true;
+ } else if (track.source._codec === 'hevc' && !decoderConfig.description) {
+ // ISOBMFF can only hold HEVC in the HEVC format, not in Annex B, but the missing description indicates
+ // Annex B. This means we'll need to do some converterino.
+
+ const decoderConfigurationRecord = extractHevcDecoderConfigurationRecord(packet.data);
+ if (!decoderConfigurationRecord) {
+ throw new Error(
+ 'Couldn\'t extract an HEVCDecoderConfigurationRecord from the HEVC packet. Make sure the packets'
+ + ' are in Annex B format (as specified in ITU-T-REC-H.265) when not providing a description, or'
+ + ' provide a description (must be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15)'
+ + ' and ensure the packets are in HEVC format.',
+ );
+ }
+
+ decoderConfig.description = serializeHevcDecoderConfigurationRecord(decoderConfigurationRecord);
+ requiresAnnexBTransformation = true;
}
// The frame rate set by the user may not be an integer. Since timescale is an integer, we'll approximate the
@@ -240,7 +259,7 @@ export class IsobmffMuxer extends Muxer {
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight,
decoderConfig: decoderConfig,
- requiresAvcTransformation,
+ requiresAnnexBTransformation,
},
timescale,
samples: [],
@@ -350,12 +369,12 @@ export class IsobmffMuxer extends Muxer {
const trackData = this.getVideoTrackData(track, packet, meta);
let packetData = packet.data;
- if (trackData.info.requiresAvcTransformation) {
- const transformedData = transformAnnexBToAvcc(packetData);
+ if (trackData.info.requiresAnnexBTransformation) {
+ const transformedData = transformAnnexBToLengthPrefixed(packetData);
if (!transformedData) {
throw new Error(
'Failed to transform packet data. Make sure all packets are provided in Annex B format, as'
- + ' specified in ITU-T-REC-H.264.',
+ + ' specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.',
);
}
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index 095b351..849026f 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -1,4 +1,8 @@
-import { extractAvcDecoderConfigurationRecord } from '../avc';
+import {
+ extractAvcDecoderConfigurationRecord,
+ extractHevcDecoderConfigurationRecord,
+ serializeHevcDecoderConfigurationRecord,
+} from '../avc';
import {
AacCodecInfo,
AudioCodec,
@@ -1552,7 +1556,9 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
= this.internalTrack.info.codec === 'vp9'
|| this.internalTrack.info.codec === 'av1'
// Packets are in Annex B format:
- || (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription);
+ || (this.internalTrack.info.codec === 'avc' && !this.internalTrack.info.codecDescription)
+ // Packets are in Annex B format:
+ || (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription);
if (needsPacketForAdditionalInfo) {
firstPacket = await this.getFirstPacket({});
@@ -1568,6 +1574,9 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid
avcCodecInfo: this.internalTrack.info.codec === 'avc' && firstPacket
? extractAvcDecoderConfigurationRecord(firstPacket.data)
: null,
+ hevcCodecInfo: this.internalTrack.info.codec === 'hevc' && firstPacket
+ ? extractHevcDecoderConfigurationRecord(firstPacket.data)
+ : null,
vp9CodecInfo: this.internalTrack.info.codec === 'vp9' && firstPacket
? extractVp9CodecInfoFromFrame(firstPacket.data)
: null,
diff --git a/src/misc.ts b/src/misc.ts
index 6701e31..854f4f2 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -104,6 +104,15 @@ export const readExpGolomb = (bitstream: Bitstream) => {
return result;
};
+/** Reads a signed exponential-Golomb universal code from a Bitstream. */
+export const readSignedExpGolomb = (bitstream: Bitstream) => {
+ const codeNum = readExpGolomb(bitstream);
+
+ return ((codeNum & 1) === 0)
+ ? -(codeNum >> 1)
+ : ((codeNum + 1) >> 1);
+};
+
export const writeBits = (bytes: Uint8Array, start: number, end: number, value: number) => {
for (let i = start; i < end; i++) {
const byteIndex = Math.floor(i / 8);
diff --git a/todo.txt b/todo.txt
index 41415e1..b29f89a 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,4 +1,5 @@
- https://github.com/Vanilagy/mp4-muxer/issues/83 tell him it's possible now
- textsubtitlesource, chunked piping
- More efficient MP3 loading when reading sequentially
-- Redo readers: I think better to have a single reader with ephemeral/non-ephemeral loading methods than to have multiple readers. This way, they can still share data.
\ No newline at end of file
+- Redo readers: I think better to have a single reader with ephemeral/non-ephemeral loading methods than to have multiple readers. This way, they can still share data.
+- Readers: Does it make more sense to have `pos` handled outside? Otherwise it's ripe for async race conditions.
\ No newline at end of file