UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

1,554 lines (1,341 loc) 92.1 kB
/*! * Copyright (c) 2026-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { AVC_LEVEL_TABLE, VideoCodec, VP9_LEVEL_TABLE } from './codec'; import { assert, assertNever, base64ToBytes, bytesToBase64, keyValueIterator, getUint24, last, readExpGolomb, readSignedExpGolomb, Rational, textDecoder, textEncoder, toDataView, toUint8Array, getChromiumVersion, isChromium, setUint24, } from './misc'; import { PacketType } from './packet'; import { MetadataTags } from './metadata'; import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../shared/ac3-misc'; import { Bitstream } from '../shared/bitstream'; // References for AVC/HEVC code: // ISO 14496-15 // Rec. ITU-T H.264 // Rec. ITU-T H.265 // https://stackoverflow.com/questions/24884827 export enum AvcNalUnitType { NON_IDR_SLICE = 1, SLICE_DPA = 2, SLICE_DPB = 3, SLICE_DPC = 4, IDR = 5, SEI = 6, SPS = 7, PPS = 8, AUD = 9, SPS_EXT = 13, } export enum HevcNalUnitType { RASL_N = 8, RASL_R = 9, BLA_W_LP = 16, RSV_IRAP_VCL23 = 23, VPS_NUT = 32, SPS_NUT = 33, PPS_NUT = 34, AUD_NUT = 35, PREFIX_SEI_NUT = 39, SUFFIX_SEI_NUT = 40, } export type NalUnitLocation = { offset: number; length: number; }; export const iterateNalUnitsInAnnexB = function* (packetData: Uint8Array): Generator<NalUnitLocation> { let i = 0; let nalStart = -1; while (i < packetData.length - 2) { const zeroIndex = packetData.indexOf(0, i); if (zeroIndex === -1 || zeroIndex >= packetData.length - 2) { break; } i = zeroIndex; let startCodeLength = 0; // Check for 4-byte start code (0x00000001) if ( i + 3 < packetData.length && packetData[i + 1] === 0 && packetData[i + 2] === 0 && packetData[i + 3] === 1 ) { startCodeLength = 4; } else if (packetData[i + 1] === 0 && packetData[i + 2] === 1) { // Check for 3-byte start code (0x000001) startCodeLength = 3; } if (startCodeLength === 0) { i++; continue; } // If we had a previous NAL unit, yield it if (nalStart !== -1 && i > nalStart) { yield { offset: nalStart, length: i - nalStart, }; } nalStart = i + startCodeLength; i = nalStart; } // Yield the last NAL unit if there is one if (nalStart !== -1 && nalStart < packetData.length) { yield { offset: nalStart, length: packetData.length - nalStart, }; } }; export const iterateNalUnitsInLengthPrefixed = function* ( packetData: Uint8Array, lengthSize: 1 | 2 | 3 | 4, ): Generator<NalUnitLocation> { let offset = 0; const dataView = new DataView(packetData.buffer, packetData.byteOffset, packetData.byteLength); while (offset + lengthSize <= packetData.length) { let nalUnitLength: number; if (lengthSize === 1) { nalUnitLength = dataView.getUint8(offset); } else if (lengthSize === 2) { nalUnitLength = dataView.getUint16(offset, false); } else if (lengthSize === 3) { nalUnitLength = getUint24(dataView, offset, false); } else { assert(lengthSize === 4); nalUnitLength = dataView.getUint32(offset, false); } offset += lengthSize; yield { offset, length: nalUnitLength, }; offset += nalUnitLength; } }; export const iterateAvcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => { if (decoderConfig.description) { const bytes = toUint8Array(decoderConfig.description); const lengthSizeMinusOne = bytes[4]! & 0b11; const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4; return iterateNalUnitsInLengthPrefixed(packetData, lengthSize); } else { return iterateNalUnitsInAnnexB(packetData); } }; export const extractNalUnitTypeForAvc = (byte: number) => { return byte & 0x1F; }; 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); }; const ANNEX_B_START_CODE = new Uint8Array([0, 0, 0, 1]); export const concatNalUnitsInAnnexB = (nalUnits: Uint8Array[]) => { const totalLength = nalUnits.reduce((a, b) => a + ANNEX_B_START_CODE.byteLength + b.byteLength, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const nalUnit of nalUnits) { result.set(ANNEX_B_START_CODE, offset); offset += ANNEX_B_START_CODE.byteLength; result.set(nalUnit, offset); offset += nalUnit.byteLength; } return result; }; export const concatNalUnitsInLengthPrefixed = (nalUnits: Uint8Array[], lengthSize: 1 | 2 | 3 | 4) => { const totalLength = nalUnits.reduce((a, b) => a + lengthSize + b.byteLength, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const nalUnit of nalUnits) { const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); switch (lengthSize) { case 1: dataView.setUint8(offset, nalUnit.byteLength); break; case 2: dataView.setUint16(offset, nalUnit.byteLength, false); break; case 3: setUint24(dataView, offset, nalUnit.byteLength, false); break; case 4: dataView.setUint32(offset, nalUnit.byteLength, false); break; } offset += lengthSize; result.set(nalUnit, offset); offset += nalUnit.byteLength; } return result; }; // Data specified in ISO 14496-15 export type AvcDecoderConfigurationRecord = { configurationVersion: number; avcProfileIndication: number; profileCompatibility: number; avcLevelIndication: number; lengthSizeMinusOne: number; sequenceParameterSets: Uint8Array[]; pictureParameterSets: Uint8Array[]; // Fields only for specific profiles: chromaFormat: number | null; bitDepthLumaMinus8: number | null; bitDepthChromaMinus8: number | null; sequenceParameterSetExt: Uint8Array[] | null; }; export const concatAvcNalUnits = (nalUnits: Uint8Array[], decoderConfig: VideoDecoderConfig) => { if (decoderConfig.description) { // Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config const bytes = toUint8Array(decoderConfig.description); const lengthSizeMinusOne = bytes[4]! & 0b11; const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4; return concatNalUnitsInLengthPrefixed(nalUnits, lengthSize); } else { // Stream is in Annex B format return concatNalUnitsInAnnexB(nalUnits); } }; /** Builds an AvcDecoderConfigurationRecord from an AVC packet in Annex B format. */ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array): AvcDecoderConfigurationRecord | null => { try { const spsUnits: Uint8Array[] = []; const ppsUnits: Uint8Array[] = []; const spsExtUnits: Uint8Array[] = []; for (const loc of iterateNalUnitsInAnnexB(packetData)) { const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length); const type = extractNalUnitTypeForAvc(nalUnit[0]!); if (type === AvcNalUnitType.SPS) { spsUnits.push(nalUnit); } else if (type === AvcNalUnitType.PPS) { ppsUnits.push(nalUnit); } else if (type === AvcNalUnitType.SPS_EXT) { spsExtUnits.push(nalUnit); } } if (spsUnits.length === 0) { return null; } if (ppsUnits.length === 0) { return null; } // Let's get the first SPS for profile and level information const spsData = spsUnits[0]!; const spsInfo = parseAvcSps(spsData); assert(spsInfo !== null); const hasExtendedData = spsInfo.profileIdc === 100 || spsInfo.profileIdc === 110 || spsInfo.profileIdc === 122 || spsInfo.profileIdc === 144; return { configurationVersion: 1, avcProfileIndication: spsInfo.profileIdc, profileCompatibility: spsInfo.constraintFlags, avcLevelIndication: spsInfo.levelIdc, lengthSizeMinusOne: 3, // Typically 4 bytes for length field sequenceParameterSets: spsUnits, pictureParameterSets: ppsUnits, chromaFormat: hasExtendedData ? spsInfo.chromaFormatIdc : null, bitDepthLumaMinus8: hasExtendedData ? spsInfo.bitDepthLumaMinus8 : null, bitDepthChromaMinus8: hasExtendedData ? spsInfo.bitDepthChromaMinus8 : null, sequenceParameterSetExt: hasExtendedData ? spsExtUnits : null, }; } catch (error) { console.error('Error building AVC Decoder Configuration Record:', error); return null; } }; /** Serializes an AvcDecoderConfigurationRecord into the format specified in Section 5.3.3.1 of ISO 14496-15. */ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigurationRecord) => { const bytes: number[] = []; // Write header bytes.push(record.configurationVersion); bytes.push(record.avcProfileIndication); bytes.push(record.profileCompatibility); bytes.push(record.avcLevelIndication); bytes.push(0xFC | (record.lengthSizeMinusOne & 0x03)); // Reserved bits (6) + lengthSizeMinusOne (2) // Reserved bits (3) + numOfSequenceParameterSets (5) bytes.push(0xE0 | (record.sequenceParameterSets.length & 0x1F)); // Write SPS for (const sps of record.sequenceParameterSets) { const length = sps.byteLength; bytes.push(length >> 8); // High byte bytes.push(length & 0xFF); // Low byte for (let i = 0; i < length; i++) { bytes.push(sps[i]!); } } bytes.push(record.pictureParameterSets.length); // Write PPS for (const pps of record.pictureParameterSets) { const length = pps.byteLength; bytes.push(length >> 8); // High byte bytes.push(length & 0xFF); // Low byte for (let i = 0; i < length; i++) { bytes.push(pps[i]!); } } if ( record.avcProfileIndication === 100 || record.avcProfileIndication === 110 || record.avcProfileIndication === 122 || record.avcProfileIndication === 144 ) { assert(record.chromaFormat !== null); assert(record.bitDepthLumaMinus8 !== null); assert(record.bitDepthChromaMinus8 !== null); assert(record.sequenceParameterSetExt !== null); bytes.push(0xFC | (record.chromaFormat & 0x03)); // Reserved bits + chroma_format bytes.push(0xF8 | (record.bitDepthLumaMinus8 & 0x07)); // Reserved bits + bit_depth_luma_minus8 bytes.push(0xF8 | (record.bitDepthChromaMinus8 & 0x07)); // Reserved bits + bit_depth_chroma_minus8 bytes.push(record.sequenceParameterSetExt.length); // Write SPS Ext for (const spsExt of record.sequenceParameterSetExt) { const length = spsExt.byteLength; bytes.push(length >> 8); // High byte bytes.push(length & 0xFF); // Low byte for (let i = 0; i < length; i++) { bytes.push(spsExt[i]!); } } } return new Uint8Array(bytes); }; /** Deserializes an AvcDecoderConfigurationRecord from the format specified in Section 5.3.3.1 of ISO 14496-15. */ export const deserializeAvcDecoderConfigurationRecord = (data: Uint8Array): AvcDecoderConfigurationRecord | null => { try { const view = toDataView(data); let offset = 0; // Read header const configurationVersion = view.getUint8(offset++); const avcProfileIndication = view.getUint8(offset++); const profileCompatibility = view.getUint8(offset++); const avcLevelIndication = view.getUint8(offset++); const lengthSizeMinusOne = view.getUint8(offset++) & 0x03; const numOfSequenceParameterSets = view.getUint8(offset++) & 0x1F; // Read SPS const sequenceParameterSets: Uint8Array[] = []; for (let i = 0; i < numOfSequenceParameterSets; i++) { const length = view.getUint16(offset, false); offset += 2; sequenceParameterSets.push(data.subarray(offset, offset + length)); offset += length; } const numOfPictureParameterSets = view.getUint8(offset++); // Read PPS const pictureParameterSets: Uint8Array[] = []; for (let i = 0; i < numOfPictureParameterSets; i++) { const length = view.getUint16(offset, false); offset += 2; pictureParameterSets.push(data.subarray(offset, offset + length)); offset += length; } const record: AvcDecoderConfigurationRecord = { configurationVersion, avcProfileIndication, profileCompatibility, avcLevelIndication, lengthSizeMinusOne, sequenceParameterSets, pictureParameterSets, chromaFormat: null, bitDepthLumaMinus8: null, bitDepthChromaMinus8: null, sequenceParameterSetExt: null, }; // Check if there are extended profile fields if ( ( avcProfileIndication === 100 || avcProfileIndication === 110 || avcProfileIndication === 122 || avcProfileIndication === 144 ) && offset + 4 <= data.length ) { const chromaFormat = view.getUint8(offset++) & 0x03; const bitDepthLumaMinus8 = view.getUint8(offset++) & 0x07; const bitDepthChromaMinus8 = view.getUint8(offset++) & 0x07; const numOfSequenceParameterSetExt = view.getUint8(offset++); record.chromaFormat = chromaFormat; record.bitDepthLumaMinus8 = bitDepthLumaMinus8; record.bitDepthChromaMinus8 = bitDepthChromaMinus8; // Read SPS Ext const sequenceParameterSetExt: Uint8Array[] = []; for (let i = 0; i < numOfSequenceParameterSetExt; i++) { const length = view.getUint16(offset, false); offset += 2; sequenceParameterSetExt.push(data.subarray(offset, offset + length)); offset += length; } record.sequenceParameterSetExt = sequenceParameterSetExt; } return record; } catch (error) { console.error('Error deserializing AVC Decoder Configuration Record:', error); return null; } }; export type AvcSpsInfo = { profileIdc: number; constraintFlags: number; levelIdc: number; frameMbsOnlyFlag: number; chromaFormatIdc: number; bitDepthLumaMinus8: number; bitDepthChromaMinus8: number; codedWidth: number; codedHeight: number; displayWidth: number; displayHeight: number; pixelAspectRatio: Rational; colourPrimaries: number; transferCharacteristics: number; matrixCoefficients: number; fullRangeFlag: number; numReorderFrames: number; maxDecFrameBuffering: number; }; const AVC_HEVC_ASPECT_RATIO_IDC_TABLE: Partial<Record<number, Rational>> = { 1: { num: 1, den: 1 }, 2: { num: 12, den: 11 }, 3: { num: 10, den: 11 }, 4: { num: 16, den: 11 }, 5: { num: 40, den: 33 }, 6: { num: 24, den: 11 }, 7: { num: 20, den: 11 }, 8: { num: 32, den: 11 }, 9: { num: 80, den: 33 }, 10: { num: 18, den: 11 }, 11: { num: 15, den: 11 }, 12: { num: 64, den: 33 }, 13: { num: 160, den: 99 }, 14: { num: 4, den: 3 }, 15: { num: 3, den: 2 }, 16: { num: 2, den: 1 }, }; /** Parses an AVC SPS (Sequence Parameter Set) to extract basic information. */ export const parseAvcSps = (sps: Uint8Array): AvcSpsInfo | null => { try { const bitstream = new Bitstream(removeEmulationPreventionBytes(sps)); bitstream.skipBits(1); // forbidden_zero_bit bitstream.skipBits(2); // nal_ref_idc const nalUnitType = bitstream.readBits(5); if (nalUnitType !== 7) { // SPS NAL unit type is 7 return null; } const profileIdc = bitstream.readAlignedByte(); const constraintFlags = bitstream.readAlignedByte(); const levelIdc = bitstream.readAlignedByte(); readExpGolomb(bitstream); // seq_parameter_set_id // "When chroma_format_idc is not present, it shall be inferred to be equal to 1 (4:2:0 chroma format)." let chromaFormatIdc = 1; // "When bit_depth_luma_minus8 is not present, it shall be inferred to be equal to 0."" let bitDepthLumaMinus8 = 0; // "When bit_depth_chroma_minus8 is not present, it shall be inferred to be equal to 0." let bitDepthChromaMinus8 = 0; // "When separate_colour_plane_flag is not present, it shall be inferred to be equal to 0." let separateColourPlaneFlag = 0; // Handle high profile chroma_format_idc if ( profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128 ) { chromaFormatIdc = readExpGolomb(bitstream); if (chromaFormatIdc === 3) { separateColourPlaneFlag = bitstream.readBits(1); } bitDepthLumaMinus8 = readExpGolomb(bitstream); bitDepthChromaMinus8 = readExpGolomb(bitstream); bitstream.skipBits(1); // qpprime_y_zero_transform_bypass_flag const seqScalingMatrixPresentFlag = bitstream.readBits(1); if (seqScalingMatrixPresentFlag) { for (let i = 0; i < (chromaFormatIdc !== 3 ? 8 : 12); i++) { const seqScalingListPresentFlag = bitstream.readBits(1); if (seqScalingListPresentFlag) { const sizeOfScalingList = i < 6 ? 16 : 64; let lastScale = 8; let nextScale = 8; for (let j = 0; j < sizeOfScalingList; j++) { if (nextScale !== 0) { const deltaScale = readSignedExpGolomb(bitstream); nextScale = (lastScale + deltaScale + 256) % 256; } lastScale = nextScale === 0 ? lastScale : nextScale; } } } } } readExpGolomb(bitstream); // log2_max_frame_num_minus4 const picOrderCntType = readExpGolomb(bitstream); if (picOrderCntType === 0) { readExpGolomb(bitstream); // log2_max_pic_order_cnt_lsb_minus4 } else if (picOrderCntType === 1) { bitstream.skipBits(1); // delta_pic_order_always_zero_flag readSignedExpGolomb(bitstream); // offset_for_non_ref_pic readSignedExpGolomb(bitstream); // offset_for_top_to_bottom_field const numRefFramesInPicOrderCntCycle = readExpGolomb(bitstream); for (let i = 0; i < numRefFramesInPicOrderCntCycle; i++) { readSignedExpGolomb(bitstream); // offset_for_ref_frame[i] } } readExpGolomb(bitstream); // max_num_ref_frames bitstream.skipBits(1); // gaps_in_frame_num_value_allowed_flag const picWidthInMbsMinus1 = readExpGolomb(bitstream); const picHeightInMapUnitsMinus1 = readExpGolomb(bitstream); const codedWidth = 16 * (picWidthInMbsMinus1 + 1); const codedHeight = 16 * (picHeightInMapUnitsMinus1 + 1); let displayWidth = codedWidth; let displayHeight = codedHeight; const frameMbsOnlyFlag = bitstream.readBits(1); if (!frameMbsOnlyFlag) { bitstream.skipBits(1); // mb_adaptive_frame_field_flag } bitstream.skipBits(1); // direct_8x8_inference_flag const frameCroppingFlag = bitstream.readBits(1); if (frameCroppingFlag) { const frameCropLeftOffset = readExpGolomb(bitstream); const frameCropRightOffset = readExpGolomb(bitstream); const frameCropTopOffset = readExpGolomb(bitstream); const frameCropBottomOffset = readExpGolomb(bitstream); let cropUnitX: number; let cropUnitY: number; const chromaArrayType = separateColourPlaneFlag === 0 ? chromaFormatIdc : 0; if (chromaArrayType === 0) { // "If ChromaArrayType is equal to 0, CropUnitX and CropUnitY are derived as:" cropUnitX = 1; cropUnitY = 2 - frameMbsOnlyFlag; } else { // "Otherwise (ChromaArrayType is equal to 1, 2, or 3), CropUnitX and CropUnitY are derived as:" const subWidthC = chromaFormatIdc === 3 ? 1 : 2; const subHeightC = chromaFormatIdc === 1 ? 2 : 1; cropUnitX = subWidthC; cropUnitY = subHeightC * (2 - frameMbsOnlyFlag); } displayWidth -= (cropUnitX * (frameCropLeftOffset + frameCropRightOffset)); displayHeight -= (cropUnitY * (frameCropTopOffset + frameCropBottomOffset)); } // 2 = unspecified let colourPrimaries = 2; let transferCharacteristics = 2; let matrixCoefficients = 2; let fullRangeFlag = 0; let pixelAspectRatio: Rational = { num: 1, den: 1 }; let numReorderFrames: number | null = null; let maxDecFrameBuffering: number | null = null; const vuiParametersPresentFlag = bitstream.readBits(1); if (vuiParametersPresentFlag) { const aspectRatioInfoPresentFlag = bitstream.readBits(1); if (aspectRatioInfoPresentFlag) { const aspectRatioIdc = bitstream.readBits(8); if (aspectRatioIdc === 255) { // Extended_SAR pixelAspectRatio = { num: bitstream.readBits(16), den: bitstream.readBits(16), }; } else { const aspectRatio = AVC_HEVC_ASPECT_RATIO_IDC_TABLE[aspectRatioIdc]; if (aspectRatio) { pixelAspectRatio = aspectRatio; } } } const overscanInfoPresentFlag = bitstream.readBits(1); if (overscanInfoPresentFlag) { bitstream.skipBits(1); // overscan_appropriate_flag } const videoSignalTypePresentFlag = bitstream.readBits(1); if (videoSignalTypePresentFlag) { bitstream.skipBits(3); // video_format fullRangeFlag = bitstream.readBits(1); const colourDescriptionPresentFlag = bitstream.readBits(1); if (colourDescriptionPresentFlag) { colourPrimaries = bitstream.readBits(8); transferCharacteristics = bitstream.readBits(8); matrixCoefficients = bitstream.readBits(8); } } const chromaLocInfoPresentFlag = bitstream.readBits(1); if (chromaLocInfoPresentFlag) { readExpGolomb(bitstream); // chroma_sample_loc_type_top_field readExpGolomb(bitstream); // chroma_sample_loc_type_bottom_field } const timingInfoPresentFlag = bitstream.readBits(1); if (timingInfoPresentFlag) { bitstream.skipBits(32); // num_units_in_tick bitstream.skipBits(32); // time_scale bitstream.skipBits(1); // fixed_frame_rate_flag } const nalHrdParametersPresentFlag = bitstream.readBits(1); if (nalHrdParametersPresentFlag) { skipAvcHrdParameters(bitstream); } const vclHrdParametersPresentFlag = bitstream.readBits(1); if (vclHrdParametersPresentFlag) { skipAvcHrdParameters(bitstream); } if (nalHrdParametersPresentFlag || vclHrdParametersPresentFlag) { bitstream.skipBits(1); // low_delay_hrd_flag } bitstream.skipBits(1); // pic_struct_present_flag const bitstreamRestrictionFlag = bitstream.readBits(1); if (bitstreamRestrictionFlag) { bitstream.skipBits(1); // motion_vectors_over_pic_boundaries_flag readExpGolomb(bitstream); // max_bytes_per_pic_denom readExpGolomb(bitstream); // max_bits_per_mb_denom readExpGolomb(bitstream); // log2_max_mv_length_horizontal readExpGolomb(bitstream); // log2_max_mv_length_vertical numReorderFrames = readExpGolomb(bitstream); maxDecFrameBuffering = readExpGolomb(bitstream); } } if (numReorderFrames === null) { assert(maxDecFrameBuffering === null); const constraintSet3Flag = constraintFlags & 0b00010000; if ( (profileIdc === 44 || profileIdc === 86 || profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 ) && constraintSet3Flag ) { // "If profile_idc is equal to 44, 86, 100, 110, 122, or 244 and constraint_set3_flag is equal to 1, the // value of num_reorder_frames shall be inferred to be equal to 0." numReorderFrames = 0; maxDecFrameBuffering = 0; } else { const picWidthInMbs = picWidthInMbsMinus1 + 1; const picHeightInMapUnits = picHeightInMapUnitsMinus1 + 1; const frameHeightInMbs = (2 - frameMbsOnlyFlag) * picHeightInMapUnits; const levelInfo = AVC_LEVEL_TABLE.find( x => x.level >= levelIdc, ) ?? last(AVC_LEVEL_TABLE)!; // "MaxDpbFrames is equal to // Min( MaxDpbMbs / ( picWidthInMbs * frameHeightInMbs ), 16 ) and MaxDpbMbs is given in Table A-1." const maxDpbFrames = Math.min( Math.floor(levelInfo.maxDpbMbs / (picWidthInMbs * frameHeightInMbs)), 16, ); // "Otherwise, [...] the value of num_reorder_frames shall be inferred to be equal to MaxDpbFrames." numReorderFrames = maxDpbFrames; maxDecFrameBuffering = maxDpbFrames; } } assert(maxDecFrameBuffering !== null); return { profileIdc, constraintFlags, levelIdc, frameMbsOnlyFlag, chromaFormatIdc, bitDepthLumaMinus8, bitDepthChromaMinus8, codedWidth, codedHeight, displayWidth, displayHeight, pixelAspectRatio, colourPrimaries, matrixCoefficients, transferCharacteristics, fullRangeFlag, numReorderFrames, maxDecFrameBuffering, }; } catch (error) { console.error('Error parsing AVC SPS:', error); return null; } }; const skipAvcHrdParameters = (bitstream: Bitstream) => { const cpb_cnt_minus1 = readExpGolomb(bitstream); bitstream.skipBits(4); // bit_rate_scale bitstream.skipBits(4); // cpb_size_scale for (let i = 0; i <= cpb_cnt_minus1; i++) { readExpGolomb(bitstream); // bit_rate_value_minus1[i] readExpGolomb(bitstream); // cpb_size_value_minus1[i] bitstream.skipBits(1); // cbr_flag[i] } bitstream.skipBits(5); // initial_cpb_removal_delay_length_minus1 bitstream.skipBits(5); // cpb_removal_delay_length_minus1 bitstream.skipBits(5); // dpb_output_delay_length_minus1 bitstream.skipBits(5); // time_offset_length }; // Data specified in ISO 14496-15 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[]; }[]; }; export type HevcSpsInfo = { displayWidth: number; displayHeight: number; pixelAspectRatio: Rational; colourPrimaries: number; transferCharacteristics: number; matrixCoefficients: number; fullRangeFlag: number; maxDecFrameBuffering: number; spsMaxSubLayersMinus1: number; spsTemporalIdNestingFlag: number; generalProfileSpace: number; generalTierFlag: number; generalProfileIdc: number; generalProfileCompatibilityFlags: number; generalConstraintIndicatorFlags: Uint8Array; generalLevelIdc: number; chromaFormatIdc: number; bitDepthLumaMinus8: number; bitDepthChromaMinus8: number; minSpatialSegmentationIdc: number; }; export const concatHevcNalUnits = (nalUnits: Uint8Array[], decoderConfig: VideoDecoderConfig) => { if (decoderConfig.description) { // Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config const bytes = toUint8Array(decoderConfig.description); const lengthSizeMinusOne = bytes[21]! & 0b11; const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4; return concatNalUnitsInLengthPrefixed(nalUnits, lengthSize); } else { // Stream is in Annex B format return concatNalUnitsInAnnexB(nalUnits); } }; export const iterateHevcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => { if (decoderConfig.description) { const bytes = toUint8Array(decoderConfig.description); const lengthSizeMinusOne = bytes[21]! & 0b11; const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4; return iterateNalUnitsInLengthPrefixed(packetData, lengthSize); } else { return iterateNalUnitsInAnnexB(packetData); } }; export const extractNalUnitTypeForHevc = (byte: number) => { return (byte >> 1) & 0x3F; }; /** Parses an HEVC SPS (Sequence Parameter Set) to extract video information. */ export const parseHevcSps = (sps: Uint8Array): HevcSpsInfo | null => { try { const bitstream = new Bitstream(removeEmulationPreventionBytes(sps)); bitstream.skipBits(16); // NAL header bitstream.readBits(4); // sps_video_parameter_set_id const spsMaxSubLayersMinus1 = bitstream.readBits(3); const spsTemporalIdNestingFlag = 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, spsMaxSubLayersMinus1); readExpGolomb(bitstream); // sps_seq_parameter_set_id const chromaFormatIdc = readExpGolomb(bitstream); let separateColourPlaneFlag = 0; if (chromaFormatIdc === 3) { separateColourPlaneFlag = bitstream.readBits(1); } const picWidthInLumaSamples = readExpGolomb(bitstream); const picHeightInLumaSamples = readExpGolomb(bitstream); let displayWidth = picWidthInLumaSamples; let displayHeight = picHeightInLumaSamples; if (bitstream.readBits(1)) { // conformance_window_flag const confWinLeftOffset = readExpGolomb(bitstream); const confWinRightOffset = readExpGolomb(bitstream); const confWinTopOffset = readExpGolomb(bitstream); const confWinBottomOffset = readExpGolomb(bitstream); // SubWidthC and SubHeightC depend on chroma_format_idc and separate_colour_plane_flag let subWidthC = 1; let subHeightC = 1; const chromaArrayType = separateColourPlaneFlag === 0 ? chromaFormatIdc : 0; if (chromaArrayType === 1) { subWidthC = 2; subHeightC = 2; } else if (chromaArrayType === 2) { subWidthC = 2; subHeightC = 1; } displayWidth -= (confWinLeftOffset + confWinRightOffset) * subWidthC; displayHeight -= (confWinTopOffset + confWinBottomOffset) * subHeightC; } const bitDepthLumaMinus8 = readExpGolomb(bitstream); const bitDepthChromaMinus8 = readExpGolomb(bitstream); readExpGolomb(bitstream); // log2_max_pic_order_cnt_lsb_minus4 const spsSubLayerOrderingInfoPresentFlag = bitstream.readBits(1); const startI = spsSubLayerOrderingInfoPresentFlag ? 0 : spsMaxSubLayersMinus1; let spsMaxNumReorderPics = 0; for (let i = startI; i <= spsMaxSubLayersMinus1; i++) { readExpGolomb(bitstream); // sps_max_dec_pic_buffering_minus1[i] spsMaxNumReorderPics = 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 numShortTermRefPicSets = readExpGolomb(bitstream); skipAllStRefPicSets(bitstream, numShortTermRefPicSets); if (bitstream.readBits(1)) { // long_term_ref_pics_present_flag const numLongTermRefPicsSps = readExpGolomb(bitstream); for (let i = 0; i < numLongTermRefPicsSps; 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 colourPrimaries = 2; let transferCharacteristics = 2; let matrixCoefficients = 2; let fullRangeFlag = 0; let minSpatialSegmentationIdc = 0; let pixelAspectRatio: Rational = { num: 1, den: 1 }; if (bitstream.readBits(1)) { // vui_parameters_present_flag const vui = parseHevcVui(bitstream, spsMaxSubLayersMinus1); pixelAspectRatio = vui.pixelAspectRatio; colourPrimaries = vui.colourPrimaries; transferCharacteristics = vui.transferCharacteristics; matrixCoefficients = vui.matrixCoefficients; fullRangeFlag = vui.fullRangeFlag; minSpatialSegmentationIdc = vui.minSpatialSegmentationIdc; } return { displayWidth, displayHeight, pixelAspectRatio, colourPrimaries, transferCharacteristics, matrixCoefficients, fullRangeFlag, maxDecFrameBuffering: spsMaxNumReorderPics + 1, spsMaxSubLayersMinus1, spsTemporalIdNestingFlag, 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, chromaFormatIdc, bitDepthLumaMinus8, bitDepthChromaMinus8, minSpatialSegmentationIdc, }; } catch (error) { console.error('Error parsing HEVC SPS:', error); return null; } }; /** Builds a HevcDecoderConfigurationRecord from an HEVC packet in Annex B format. */ export const extractHevcDecoderConfigurationRecord = (packetData: Uint8Array) => { try { const vpsUnits: Uint8Array[] = []; const spsUnits: Uint8Array[] = []; const ppsUnits: Uint8Array[] = []; const seiUnits: Uint8Array[] = []; for (const loc of iterateNalUnitsInAnnexB(packetData)) { const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length); const type = extractNalUnitTypeForHevc(nalUnit[0]!); if (type === HevcNalUnitType.VPS_NUT) { vpsUnits.push(nalUnit); } else if (type === HevcNalUnitType.SPS_NUT) { spsUnits.push(nalUnit); } else if (type === HevcNalUnitType.PPS_NUT) { ppsUnits.push(nalUnit); } else if (type === HevcNalUnitType.PREFIX_SEI_NUT || type === HevcNalUnitType.SUFFIX_SEI_NUT) { seiUnits.push(nalUnit); } } if (spsUnits.length === 0 || ppsUnits.length === 0) return null; const spsInfo = parseHevcSps(spsUnits[0]!); if (!spsInfo) return null; // 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: HevcNalUnitType.VPS_NUT, nalUnits: vpsUnits, }, ] : []), ...(spsUnits.length ? [ { arrayCompleteness: 1, nalUnitType: HevcNalUnitType.SPS_NUT, nalUnits: spsUnits, }, ] : []), ...(ppsUnits.length ? [ { arrayCompleteness: 1, nalUnitType: HevcNalUnitType.PPS_NUT, nalUnits: ppsUnits, }, ] : []), ...(seiUnits.length ? [ { arrayCompleteness: 1, nalUnitType: extractNalUnitTypeForHevc(seiUnits[0]![0]!), nalUnits: seiUnits, }, ] : []), ]; const record: HevcDecoderConfigurationRecord = { configurationVersion: 1, generalProfileSpace: spsInfo.generalProfileSpace, generalTierFlag: spsInfo.generalTierFlag, generalProfileIdc: spsInfo.generalProfileIdc, generalProfileCompatibilityFlags: spsInfo.generalProfileCompatibilityFlags, generalConstraintIndicatorFlags: spsInfo.generalConstraintIndicatorFlags, generalLevelIdc: spsInfo.generalLevelIdc, minSpatialSegmentationIdc: spsInfo.minSpatialSegmentationIdc, parallelismType, chromaFormatIdc: spsInfo.chromaFormatIdc, bitDepthLumaMinus8: spsInfo.bitDepthLumaMinus8, bitDepthChromaMinus8: spsInfo.bitDepthChromaMinus8, avgFrameRate: 0, constantFrameRate: 0, numTemporalLayers: spsInfo.spsMaxSubLayersMinus1 + 1, temporalIdNested: spsInfo.spsTemporalIdNestingFlag, 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 parseHevcVui = (bitstream: Bitstream, sps_max_sub_layers_minus1: number) => { // Defaults: 2 = unspecified let colourPrimaries = 2; let transferCharacteristics = 2; let matrixCoefficients = 2; let fullRangeFlag = 0; let minSpatialSegmentationIdc = 0; let pixelAspectRatio: Rational = { num: 1, den: 1 }; if (bitstream.readBits(1)) { // aspect_ratio_info_present_flag const aspect_ratio_idc = bitstream.readBits(8); if (aspect_ratio_idc === 255) { pixelAspectRatio = { num: bitstream.readBits(16), den: bitstream.readBits(16), }; } else { const aspectRatio = AVC_HEVC_ASPECT_RATIO_IDC_TABLE[aspect_ratio_idc]; if (aspectRatio) { pixelAspectRatio = aspectRatio; } } } 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 fullRangeFlag = bitstream.readBits(1); if (bitstream.readBits(1)) { // colour_description_present_flag colourPrimaries = bitstream.readBits(8); transferCharacteristics = bitstream.readBits(8); matrixCoefficients = bitstream.readBits(8); } } 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)) { skipHevcHrdParameters(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 minSpatialSegmentationIdc = readExpGolomb(bitstream); 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 { pixelAspectRatio, colourPrimaries, transferCharacteristics, matrixCoefficients, fullRangeFlag, minSpatialSegmentationIdc, }; }; const skipHevcHrdParameters = ( 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] } }; /** Serializes an HevcDecoderConfigurationRecord into the format specified in Section 8.3.3.1 of ISO 14496-15. */ 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); }; /** Deserializes an HevcDecoderConfigurationRecord from the format specified in Section 8.3.3.1 of ISO 14496-15. */ export const deserializeHevcDecoderConfigurationRecord = (data: Uint8Array): HevcDecoderConfigurationRecord | null => { try { const view = toDataView(data); let offset = 0; const configurationVersion = view.getUint8(offset++); const byte1 = view.getUint8(offset++); const generalProfileSpace = (byte1 >> 6) & 0x3; const generalTierFlag = (byte1 >> 5) & 0x1; const generalProfileIdc = byte1 & 0x1F; const generalProfileCompatibilityFlags = view.getUint32(offset, false); offset += 4; const generalConstraintIndicatorFlags = data.subarray(offset, offset + 6); offset += 6; const generalLevelIdc = view.getUint8(offset++); const minSpatialSegmentationIdc = ((view.getUint8(offset++) & 0x0F) << 8) | view.getUint8(offset++); const parallelismType = view.getUint8(offset++) & 0x03; const chromaFormatIdc = view.getUint8(offset++) & 0x03; const bitDepthLumaMinus8 = view.getUin