Files
mediabunny/src/codec-data.ts
T

3672 lines
106 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* 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, DtsFourCc, VideoCodec, VP9_LEVEL_TABLE } from './codec';
import {
assert,
assertNever,
base64ToBytes,
bytesToBase64,
keyValueIterator,
getUint24,
last,
readExpGolomb,
readSignedExpGolomb,
Rational,
textDecoder,
textEncoder,
toDataView,
toUint8Array,
getChromiumVersion,
isChromium,
popcount,
setUint24,
} from './misc';
import { Logging } from './logging';
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) {
Logging._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) {
Logging._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) {
Logging._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) {
Logging._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) {
Logging._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.getUint8(offset++) & 0x07;
const bitDepthChromaMinus8 = view.getUint8(offset++) & 0x07;
const avgFrameRate = view.getUint16(offset, false);
offset += 2;
const byte21 = view.getUint8(offset++);
const constantFrameRate = (byte21 >> 6) & 0x03;
const numTemporalLayers = (byte21 >> 3) & 0x07;
const temporalIdNested = (byte21 >> 2) & 0x01;
const lengthSizeMinusOne = byte21 & 0x03;
const numOfArrays = view.getUint8(offset++);
const arrays: HevcDecoderConfigurationRecord['arrays'] = [];
for (let i = 0; i < numOfArrays; i++) {
const arrByte = view.getUint8(offset++);
const arrayCompleteness = (arrByte >> 7) & 0x01;
const nalUnitType = arrByte & 0x3F;
const numNalus = view.getUint16(offset, false);
offset += 2;
const nalUnits: Uint8Array[] = [];
for (let j = 0; j < numNalus; j++) {
const nalUnitLength = view.getUint16(offset, false);
offset += 2;
nalUnits.push(data.subarray(offset, offset + nalUnitLength));
offset += nalUnitLength;
}
arrays.push({
arrayCompleteness,
nalUnitType,
nalUnits,
});
}
return {
configurationVersion,
generalProfileSpace,
generalTierFlag,
generalProfileIdc,
generalProfileCompatibilityFlags,
generalConstraintIndicatorFlags,
generalLevelIdc,
minSpatialSegmentationIdc,
parallelismType,
chromaFormatIdc,
bitDepthLumaMinus8,
bitDepthChromaMinus8,
avgFrameRate,
constantFrameRate,
numTemporalLayers,
temporalIdNested,
lengthSizeMinusOne,
arrays,
};
} catch (error) {
Logging._error('Error deserializing HEVC Decoder Configuration Record:', error);
return null;
}
};
enum HevcNaluOrderState {
audAllowed,
beforeFirstVcl,
afterFirstVcl,
eoBitstreamAllowed,
noMoreDataAllowed,
}
// This function sanitzes the contents of an HEVC packet such that
// https://source.chromium.org/chromium/chromium/src/+/main:media/formats/mp4/hevc.cc's validation logic does not trip
// up on its contents. The validation is often too strict and rejects packets that Chromium could decode just fine.
// Chromium code retrieved on 2026-04-29.
// See https://issues.chromium.org/issues/507611247.
export const sanitizeHevcPacketForChromium = (
packetData: Uint8Array,
decoderConfig: VideoDecoderConfig,
): Uint8Array | null => {
const removedNalUnits = new Set<number>();
let orderState: HevcNaluOrderState = HevcNaluOrderState.audAllowed;
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
if (orderState === HevcNaluOrderState.noMoreDataAllowed) {
removedNalUnits.add(loc.offset);
continue;
}
const type = extractNalUnitTypeForHevc(packetData[loc.offset]!);
if (orderState === HevcNaluOrderState.eoBitstreamAllowed && type !== 37 /* EOB_NUT */) {
removedNalUnits.add(loc.offset);
continue;
}
let remove = false;
if (type === 35) { // AUD_NUT
if (orderState > HevcNaluOrderState.audAllowed) {
remove = true;
} else {
orderState = HevcNaluOrderState.beforeFirstVcl;
}
} else if (type <= 31) { // VCL (0-31)
if (orderState > HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.afterFirstVcl;
}
} else if (type === 36) { // EOS_NUT
if (orderState !== HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.eoBitstreamAllowed;
}
} else if (type === 37) { // EOB_NUT
if (orderState < HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.noMoreDataAllowed;
}
} else if (
type === 32 || type === 33 || type === 34 || type === 39
|| (type >= 41 && type <= 44) || (type >= 48 && type <= 55)
) { // VPS, SPS, PPS, PREFIX_SEI, RSV_NVCL41..44, UNSPEC48..55
if (orderState > HevcNaluOrderState.beforeFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.beforeFirstVcl;
}
} else if (
type === 38 || type === 40
|| (type >= 45 && type <= 47) || (type >= 56 && type <= 63)
) { // FD, SUFFIX_SEI, RSV_NVCL45..47, UNSPEC56..63
if (orderState < HevcNaluOrderState.afterFirstVcl) {
remove = true;
}
}
if (remove) {
removedNalUnits.add(loc.offset);
}
}
// If nothing violated the rules, return null to signal that
if (removedNalUnits.size === 0) {
return null;
}
const filteredNalUnits: Uint8Array[] = [];
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
if (!removedNalUnits.has(loc.offset)) {
filteredNalUnits.push(packetData.subarray(loc.offset, loc.offset + loc.length));
}
}
return concatHevcNalUnits(filteredNalUnits, decoderConfig);
};
export type Vp9CodecInfo = {
profile: number;
level: number;
bitDepth: number;
chromaSubsampling: number;
videoFullRangeFlag: number;
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
};
export const extractVp9CodecInfoFromPacket = (
packet: Uint8Array,
): Vp9CodecInfo | null => {
// eslint-disable-next-line @stylistic/max-len
// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf
// http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
const bitstream = new Bitstream(packet);
// Frame marker (0b10)
const frameMarker = bitstream.readBits(2);
if (frameMarker !== 2) {
return null;
}
// Profile
const profileLowBit = bitstream.readBits(1);
const profileHighBit = bitstream.readBits(1);
const profile = (profileHighBit << 1) + profileLowBit;
// Skip reserved bit for profile 3
if (profile === 3) {
bitstream.skipBits(1);
}
// show_existing_frame
const showExistingFrame = bitstream.readBits(1);
if (showExistingFrame === 1) {
return null;
}
// frame_type (0 = key frame)
const frameType = bitstream.readBits(1);
if (frameType !== 0) {
return null;
}
// Skip show_frame and error_resilient_mode
bitstream.skipBits(2);
// Sync code (0x498342)
const syncCode = bitstream.readBits(24);
if (syncCode !== 0x498342) {
return null;
}
// Color config
let bitDepth = 8;
if (profile >= 2) {
const tenOrTwelveBit = bitstream.readBits(1);
bitDepth = tenOrTwelveBit ? 12 : 10;
}
// Color space
const colorSpace = bitstream.readBits(3);
let chromaSubsampling = 0;
let videoFullRangeFlag = 0;
if (colorSpace !== 7) { // 7 is CS_RGB
const colorRange = bitstream.readBits(1);
videoFullRangeFlag = colorRange;
if (profile === 1 || profile === 3) {
const subsamplingX = bitstream.readBits(1);
const subsamplingY = bitstream.readBits(1);
// 0 = 4:2:0 vertical
// 1 = 4:2:0 colocated
// 2 = 4:2:2
// 3 = 4:4:4
chromaSubsampling = !subsamplingX && !subsamplingY
? 3 // 0,0 = 4:4:4
: subsamplingX && !subsamplingY
? 2 // 1,0 = 4:2:2
: 1; // 1,1 = 4:2:0 colocated (default)
// Skip reserved bit
bitstream.skipBits(1);
} else {
// For profile 0 and 2, always 4:2:0
chromaSubsampling = 1; // Using colocated as default
}
} else {
// RGB is always 4:4:4
chromaSubsampling = 3;
videoFullRangeFlag = 1;
}
// Parse frame size
const widthMinusOne = bitstream.readBits(16);
const heightMinusOne = bitstream.readBits(16);
const width = widthMinusOne + 1;
const height = heightMinusOne + 1;
// Calculate level based on dimensions
const pictureSize = width * height;
let level = last(VP9_LEVEL_TABLE)!.level; // Default to highest level
for (const entry of VP9_LEVEL_TABLE) {
if (pictureSize <= entry.maxPictureSize) {
level = entry.level;
break;
}
}
// Map color_space to standard values
const matrixCoefficients = colorSpace === 7
? 0
: colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
const colourPrimaries = colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
const transferCharacteristics = colorSpace === 2
? 1
: colorSpace === 1
? 6
: 2;
return {
profile,
level,
bitDepth,
chromaSubsampling,
videoFullRangeFlag,
colourPrimaries,
transferCharacteristics,
matrixCoefficients,
};
};
export type Av1CodecInfo = {
profile: number;
level: number;
tier: number;
bitDepth: number;
monochrome: number;
chromaSubsamplingX: number;
chromaSubsamplingY: number;
chromaSamplePosition: number;
};
/** Iterates over all OBUs in an AV1 packet bitstream. */
export const iterateAv1PacketObus = function* (packet: Uint8Array) {
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf
const bitstream = new Bitstream(packet);
const readLeb128 = (): number | null => {
let value = 0;
for (let i = 0; i < 8; i++) {
const byte = bitstream.readAlignedByte();
value |= ((byte & 0x7f) << (i * 7));
if (!(byte & 0x80)) {
break;
}
// Spec requirement
if (i === 7 && (byte & 0x80)) {
return null;
}
}
// Spec requirement
if (value >= 2 ** 32 - 1) {
return null;
}
return value;
};
while (bitstream.getBitsLeft() >= 8) {
// Parse OBU header
bitstream.skipBits(1);
const obuType = bitstream.readBits(4);
const obuExtension = bitstream.readBits(1);
const obuHasSizeField = bitstream.readBits(1);
bitstream.skipBits(1);
// Skip extension header if present
if (obuExtension) {
bitstream.skipBits(8);
}
// Read OBU size if present
let obuSize: number;
if (obuHasSizeField) {
const obuSizeValue = readLeb128();
if (obuSizeValue === null) return; // It was invalid
obuSize = obuSizeValue;
} else {
// Calculate remaining bits and convert to bytes, rounding down
obuSize = Math.floor(bitstream.getBitsLeft() / 8);
}
assert(bitstream.pos % 8 === 0);
yield {
type: obuType,
data: packet.subarray(bitstream.pos / 8, bitstream.pos / 8 + obuSize),
};
// Move to next OBU
bitstream.skipBits(obuSize * 8);
}
};
/**
* When AV1 codec information is not provided by the container, we can still try to extract the information by digging
* into the AV1 bitstream.
*/
export const extractAv1CodecInfoFromPacket = (
packet: Uint8Array,
): Av1CodecInfo | null => {
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf
for (const { type, data } of iterateAv1PacketObus(packet)) {
if (type !== 1) {
continue; // 1 == OBU_SEQUENCE_HEADER
}
const bitstream = new Bitstream(data);
// Read sequence header fields
const seqProfile = bitstream.readBits(3);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const stillPicture = bitstream.readBits(1);
const reducedStillPictureHeader = bitstream.readBits(1);
let seqLevel = 0;
let seqTier = 0;
let bufferDelayLengthMinus1 = 0;
if (reducedStillPictureHeader) {
seqLevel = bitstream.readBits(5);
} else {
// Parse timing_info_present_flag
const timingInfoPresentFlag = bitstream.readBits(1);
if (timingInfoPresentFlag) {
// Skip timing info (num_units_in_display_tick, time_scale, equal_picture_interval)
bitstream.skipBits(32); // num_units_in_display_tick
bitstream.skipBits(32); // time_scale
const equalPictureInterval = bitstream.readBits(1);
if (equalPictureInterval) {
// Skip num_ticks_per_picture_minus_1 (uvlc)
// Since this is variable length, we'd need to implement uvlc reading
// For now, we'll return null as this is rare
return null;
}
}
// Parse decoder_model_info_present_flag
const decoderModelInfoPresentFlag = bitstream.readBits(1);
if (decoderModelInfoPresentFlag) {
// Store buffer_delay_length_minus_1 instead of just skipping
bufferDelayLengthMinus1 = bitstream.readBits(5);
bitstream.skipBits(32); // num_units_in_decoding_tick
bitstream.skipBits(5); // buffer_removal_time_length_minus_1
bitstream.skipBits(5); // frame_presentation_time_length_minus_1
}
// Parse operating_points_cnt_minus_1
const operatingPointsCntMinus1 = bitstream.readBits(5);
// For each operating point
for (let i = 0; i <= operatingPointsCntMinus1; i++) {
// operating_point_idc[i]
bitstream.skipBits(12);
// seq_level_idx[i]
const seqLevelIdx = bitstream.readBits(5);
if (i === 0) {
seqLevel = seqLevelIdx;
}
if (seqLevelIdx > 7) {
// seq_tier[i]
const seqTierTemp = bitstream.readBits(1);
if (i === 0) {
seqTier = seqTierTemp;
}
}
if (decoderModelInfoPresentFlag) {
// decoder_model_present_for_this_op[i]
const decoderModelPresentForThisOp = bitstream.readBits(1);
if (decoderModelPresentForThisOp) {
const n = bufferDelayLengthMinus1 + 1;
bitstream.skipBits(n); // decoder_buffer_delay[op]
bitstream.skipBits(n); // encoder_buffer_delay[op]
bitstream.skipBits(1); // low_delay_mode_flag[op]
}
}
// initial_display_delay_present_flag
const initialDisplayDelayPresentFlag = bitstream.readBits(1);
if (initialDisplayDelayPresentFlag) {
// initial_display_delay_minus_1[i]
bitstream.skipBits(4);
}
}
}
// Frame size
const frameWidthBitsMinus1 = bitstream.readBits(4);
const frameHeightBitsMinus1 = bitstream.readBits(4);
const n1 = frameWidthBitsMinus1 + 1;
bitstream.skipBits(n1); // max_frame_width_minus_1
const n2 = frameHeightBitsMinus1 + 1;
bitstream.skipBits(n2); // max_frame_height_minus_1
// Frame IDs
let frameIdNumbersPresentFlag = 0;
if (reducedStillPictureHeader) {
frameIdNumbersPresentFlag = 0;
} else {
frameIdNumbersPresentFlag = bitstream.readBits(1);
}
if (frameIdNumbersPresentFlag) {
bitstream.skipBits(4); // delta_frame_id_length_minus_2
bitstream.skipBits(3); // additional_frame_id_length_minus_1
}
bitstream.skipBits(1); // use_128x128_superblock
bitstream.skipBits(1); // enable_filter_intra
bitstream.skipBits(1); // enable_intra_edge_filter
if (!reducedStillPictureHeader) {
bitstream.skipBits(1); // enable_interintra_compound
bitstream.skipBits(1); // enable_masked_compound
bitstream.skipBits(1); // enable_warped_motion
bitstream.skipBits(1); // enable_dual_filter
const enableOrderHint = bitstream.readBits(1);
if (enableOrderHint) {
bitstream.skipBits(1); // enable_jnt_comp
bitstream.skipBits(1); // enable_ref_frame_mvs
}
const seqChooseScreenContentTools = bitstream.readBits(1);
let seqForceScreenContentTools = 0;
if (seqChooseScreenContentTools) {
seqForceScreenContentTools = 2; // SELECT_SCREEN_CONTENT_TOOLS
} else {
seqForceScreenContentTools = bitstream.readBits(1);
}
if (seqForceScreenContentTools > 0) {
const seqChooseIntegerMv = bitstream.readBits(1);
if (!seqChooseIntegerMv) {
bitstream.skipBits(1); // seq_force_integer_mv
}
}
if (enableOrderHint) {
bitstream.skipBits(3); // order_hint_bits_minus_1
}
}
bitstream.skipBits(1); // enable_superres
bitstream.skipBits(1); // enable_cdef
bitstream.skipBits(1); // enable_restoration
// color_config()
const highBitdepth = bitstream.readBits(1);
let bitDepth = 8;
if (seqProfile === 2 && highBitdepth) {
const twelveBit = bitstream.readBits(1);
bitDepth = twelveBit ? 12 : 10;
} else if (seqProfile <= 2) {
bitDepth = highBitdepth ? 10 : 8;
}
let monochrome = 0;
if (seqProfile !== 1) {
monochrome = bitstream.readBits(1);
}
let chromaSubsamplingX = 1;
let chromaSubsamplingY = 1;
let chromaSamplePosition = 0;
if (!monochrome) {
if (seqProfile === 0) {
chromaSubsamplingX = 1;
chromaSubsamplingY = 1;
} else if (seqProfile === 1) {
chromaSubsamplingX = 0;
chromaSubsamplingY = 0;
} else {
if (bitDepth === 12) {
chromaSubsamplingX = bitstream.readBits(1);
if (chromaSubsamplingX) {
chromaSubsamplingY = bitstream.readBits(1);
}
}
}
if (chromaSubsamplingX && chromaSubsamplingY) {
chromaSamplePosition = bitstream.readBits(2);
}
}
return {
profile: seqProfile,
level: seqLevel,
tier: seqTier,
bitDepth,
monochrome,
chromaSubsamplingX,
chromaSubsamplingY,
chromaSamplePosition,
};
}
return null;
};
export const parseOpusIdentificationHeader = (bytes: Uint8Array) => {
const view = toDataView(bytes);
const outputChannelCount = view.getUint8(9);
const preSkip = view.getUint16(10, true);
const inputSampleRate = view.getUint32(12, true);
const outputGain = view.getInt16(16, true);
const channelMappingFamily = view.getUint8(18);
let channelMappingTable: Uint8Array | null = null;
if (channelMappingFamily) {
channelMappingTable = bytes.subarray(19, 19 + 2 + outputChannelCount);
}
return {
outputChannelCount,
preSkip,
inputSampleRate,
outputGain,
channelMappingFamily,
channelMappingTable,
};
};
// From https://datatracker.ietf.org/doc/html/rfc6716, in 48 kHz samples
const OPUS_FRAME_DURATION_TABLE = [
480, 960, 1920, 2880,
480, 960, 1920, 2880,
480, 960, 1920, 2880,
480, 960,
480, 960,
120, 240, 480, 960,
120, 240, 480, 960,
120, 240, 480, 960,
120, 240, 480, 960,
];
export const parseOpusTocByte = (packet: Uint8Array) => {
const config = packet[0]! >> 3;
const code = packet[0]! & 0b11;
// A packet may pack more than one frame, in which case its duration is the frame duration times the number of
// frames it carries. See https://datatracker.ietf.org/doc/html/rfc6716, section 3.2.
let frameCount: number;
if (code === 0) {
frameCount = 1;
} else if (code === 1 || code === 2) {
frameCount = 2;
} else {
// Code 3: the frame count sits in the six low bits of the frame count byte
frameCount = packet[1]! & 0b111111;
}
return {
durationInSamples: OPUS_FRAME_DURATION_TABLE[config]! * frameCount,
};
};
// Based on vorbis_parser.c from FFmpeg.
export const parseModesFromVorbisSetupPacket = (setupHeader: Uint8Array) => {
// Verify that this is a Setup header.
if (setupHeader.length < 7) {
throw new Error('Setup header is too short.');
}
if (setupHeader[0] !== 5) {
throw new Error('Wrong packet type in Setup header.');
}
const signature = String.fromCharCode(...setupHeader.slice(1, 7));
if (signature !== 'vorbis') {
throw new Error('Invalid packet signature in Setup header.');
}
// Reverse the entire buffer.
const bufSize = setupHeader.length;
const revBuffer = new Uint8Array(bufSize);
for (let i = 0; i < bufSize; i++) {
revBuffer[i] = setupHeader[bufSize - 1 - i]!;
}
// Initialize a Bitstream on the reversed buffer.
const bitstream = new Bitstream(revBuffer);
// --- Find the framing bit.
// In FFmpeg code, we scan until get_bits1() returns 1.
let gotFramingBit = 0;
while (bitstream.getBitsLeft() > 97) {
if (bitstream.readBits(1) === 1) {
gotFramingBit = bitstream.pos;
break;
}
}
if (gotFramingBit === 0) {
throw new Error('Invalid Setup header: framing bit not found.');
}
// --- Search backwards for a valid mode header.
// We try to “guess” the number of modes by reading a fixed pattern.
let modeCount = 0;
let gotModeHeader = false;
let lastModeCount = 0;
while (bitstream.getBitsLeft() >= 97) {
const tempPos = bitstream.pos;
const a = bitstream.readBits(8);
const b = bitstream.readBits(16);
const c = bitstream.readBits(16);
// If a > 63 or b or c nonzero, assume we’ve gone too far.
if (a > 63 || b !== 0 || c !== 0) {
bitstream.pos = tempPos;
break;
}
bitstream.skipBits(1);
modeCount++;
if (modeCount > 64) {
break;
}
const bsClone = bitstream.clone();
const candidate = bsClone.readBits(6) + 1;
if (candidate === modeCount) {
gotModeHeader = true;
lastModeCount = modeCount;
}
}
if (!gotModeHeader) {
throw new Error('Invalid Setup header: mode header not found.');
}
if (lastModeCount > 63) {
throw new Error(`Unsupported mode count: ${lastModeCount}.`);
}
const finalModeCount = lastModeCount;
// --- Reinitialize the bitstream.
bitstream.pos = 0;
// Skip the bits up to the found framing bit.
bitstream.skipBits(gotFramingBit);
// --- Now read, for each mode (in reverse order), 40 bits then one bit.
// That one bit is the mode blockflag.
const modeBlockflags = Array(finalModeCount).fill(0) as number[];
for (let i = finalModeCount - 1; i >= 0; i--) {
bitstream.skipBits(40);
modeBlockflags[i] = bitstream.readBits(1);
}
return { modeBlockflags };
};
/** Determines a packet's type (key or delta) by digging into the packet bitstream. */
export const determineVideoPacketType = (
codec: VideoCodec,
decoderConfig: VideoDecoderConfig,
packetData: Uint8Array,
): PacketType | null => {
switch (codec) {
case 'avc': {
for (const loc of iterateAvcNalUnits(packetData, decoderConfig)) {
const nalTypeByte = packetData[loc.offset]!;
const type = extractNalUnitTypeForAvc(nalTypeByte);
if (type >= AvcNalUnitType.NON_IDR_SLICE && type <= AvcNalUnitType.SLICE_DPC) {
return 'delta';
}
if (type === AvcNalUnitType.IDR) {
return 'key';
}
// In addition to IDR, Recovery Point SEI also counts as a valid H.264 keyframe by current consensus.
// See https://github.com/w3c/webcodecs/issues/650 for the relevant discussion. WebKit and Firefox have
// always supported them, but Chromium hasn't, therefore the (admittedly dirty) version check.
if (type === AvcNalUnitType.SEI && (!isChromium() || getChromiumVersion()! >= 144)) {
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
const bytes = removeEmulationPreventionBytes(nalUnit);
let pos = 1; // Skip NALU header
// sei_rbsp()
do {
// sei_message()
let payloadType = 0;
while (true) {
const nextByte = bytes[pos++];
if (nextByte === undefined) break;
payloadType += nextByte;
if (nextByte < 255) {
break;
}
}
let payloadSize = 0;
while (true) {
const nextByte = bytes[pos++];
if (nextByte === undefined) break;
payloadSize += nextByte;
if (nextByte < 255) {
break;
}
}
// sei_payload()
const PAYLOAD_TYPE_RECOVERY_POINT = 6;
if (payloadType === PAYLOAD_TYPE_RECOVERY_POINT) {
const bitstream = new Bitstream(bytes);
bitstream.pos = 8 * pos;
const recoveryFrameCount = readExpGolomb(bitstream);
const exactMatchFlag = bitstream.readBits(1);
if (recoveryFrameCount === 0 && exactMatchFlag === 1) {
// https://github.com/w3c/webcodecs/pull/910
// "recovery_frame_cnt == 0 and exact_match_flag=1 in the SEI recovery payload"
return 'key';
}
}
pos += payloadSize;
} while (pos < bytes.length - 1);
}
}
return 'delta';
};
case 'hevc': {
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
const type = extractNalUnitTypeForHevc(packetData[loc.offset]!);
if (type < HevcNalUnitType.BLA_W_LP) {
return 'delta';
}
if (type <= HevcNalUnitType.RSV_IRAP_VCL23) {
return 'key';
}
}
return 'delta';
};
case 'vp8': {
// VP8, once again, by far the easiest to deal with.
const frameType = packetData[0]! & 0b1;
return frameType === 0 ? 'key' : 'delta';
};
case 'vp9': {
const bitstream = new Bitstream(packetData);
if (bitstream.readBits(2) !== 2) {
return null;
};
const profileLowBit = bitstream.readBits(1);
const profileHighBit = bitstream.readBits(1);
const profile = (profileHighBit << 1) + profileLowBit;
// Skip reserved bit for profile 3
if (profile === 3) {
bitstream.skipBits(1);
}
const showExistingFrame = bitstream.readBits(1);
if (showExistingFrame) {
return null;
}
const frameType = bitstream.readBits(1);
return frameType === 0 ? 'key' : 'delta';
};
case 'av1': {
let reducedStillPictureHeader = false;
for (const { type, data } of iterateAv1PacketObus(packetData)) {
if (type === 1) { // OBU_SEQUENCE_HEADER
const bitstream = new Bitstream(data);
bitstream.skipBits(4);
reducedStillPictureHeader = !!bitstream.readBits(1);
} else if (
type === 3 // OBU_FRAME_HEADER
|| type === 6 // OBU_FRAME
|| type === 7 // OBU_REDUNDANT_FRAME_HEADER
) {
if (reducedStillPictureHeader) {
return 'key';
}
const bitstream = new Bitstream(data);
const showExistingFrame = bitstream.readBits(1);
if (showExistingFrame) {
return null;
}
const frameType = bitstream.readBits(2);
return frameType === 0 ? 'key' : 'delta';
}
}
return null;
};
case 'prores': {
return 'key';
};
default: {
assertNever(codec);
assert(false);
};
}
};
export enum FlacBlockType {
STREAMINFO = 0,
VORBIS_COMMENT = 4,
PICTURE = 6,
}
export const readVorbisComments = (bytes: Uint8Array, metadataTags: MetadataTags) => {
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.2
const commentView = toDataView(bytes);
let commentPos = 0;
const vendorStringLength = commentView.getUint32(commentPos, true);
commentPos += 4;
const vendorString = textDecoder.decode(
bytes.subarray(commentPos, commentPos + vendorStringLength),
);
commentPos += vendorStringLength;
if (vendorStringLength > 0) {
// Expose the vendor string in the raw metadata
metadataTags.raw ??= {};
metadataTags.raw['vendor'] ??= vendorString;
}
const listLength = commentView.getUint32(commentPos, true);
commentPos += 4;
// Loop over all metadata tags
for (let i = 0; i < listLength; i++) {
const stringLength = commentView.getUint32(commentPos, true);
commentPos += 4;
const string = textDecoder.decode(
bytes.subarray(commentPos, commentPos + stringLength),
);
commentPos += stringLength;
const separatorIndex = string.indexOf('=');
if (separatorIndex === -1) {
continue;
}
const key = string.slice(0, separatorIndex).toUpperCase();
const value = string.slice(separatorIndex + 1);
metadataTags.raw ??= {};
metadataTags.raw[key] ??= value;
switch (key) {
case 'TITLE': {
metadataTags.title ??= value;
}; break;
case 'DESCRIPTION': {
metadataTags.description ??= value;
}; break;
case 'ARTIST': {
metadataTags.artist ??= value;
}; break;
case 'ALBUM': {
metadataTags.album ??= value;
}; break;
case 'ALBUMARTIST': {
metadataTags.albumArtist ??= value;
}; break;
case 'COMMENT': {
metadataTags.comment ??= value;
}; break;
case 'LYRICS': {
metadataTags.lyrics ??= value;
}; break;
case 'TRACKNUMBER': {
const parts = value.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
metadataTags.trackNumber ??= trackNum;
}
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
case 'TRACKTOTAL': {
const tracksTotal = Number.parseInt(value, 10);
if (Number.isInteger(tracksTotal) && tracksTotal > 0) {
metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
case 'DISCNUMBER': {
const parts = value.split('/');
const discNum = Number.parseInt(parts[0]!, 10);
const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
metadataTags.discNumber ??= discNum;
}
if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
metadataTags.discsTotal ??= discsTotal;
}
}; break;
case 'DISCTOTAL': {
const discsTotal = Number.parseInt(value, 10);
if (Number.isInteger(discsTotal) && discsTotal > 0) {
metadataTags.discsTotal ??= discsTotal;
}
}; break;
case 'DATE': {
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
metadataTags.date ??= date;
}
}; break;
case 'GENRE': {
metadataTags.genre ??= value;
}; break;
case 'METADATA_BLOCK_PICTURE': {
// https://datatracker.ietf.org/doc/rfc9639/ Section 8.8
const decoded = base64ToBytes(value);
const view = toDataView(decoded);
const pictureType = view.getUint32(0, false);
const mediaTypeLength = view.getUint32(4, false);
const mediaType = String.fromCharCode(...decoded.subarray(8, 8 + mediaTypeLength)); // ASCII
const descriptionLength = view.getUint32(8 + mediaTypeLength, false);
const description = textDecoder.decode(decoded.subarray(
12 + mediaTypeLength,
12 + mediaTypeLength + descriptionLength,
));
const dataLength = view.getUint32(mediaTypeLength + descriptionLength + 28);
const data = decoded.subarray(
mediaTypeLength + descriptionLength + 32,
mediaTypeLength + descriptionLength + 32 + dataLength,
);
metadataTags.images ??= [];
metadataTags.images.push({
data,
mimeType: mediaType,
kind: pictureType === 3 ? 'coverFront' : pictureType === 4 ? 'coverBack' : 'unknown',
name: undefined,
description: description || undefined,
});
}; break;
}
}
};
export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags, writeImages: boolean) => {
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.2
const commentHeaderParts: Uint8Array[] = [
headerBytes,
];
const vendorString = 'Mediabunny';
const encodedVendorString = textEncoder.encode(vendorString);
let currentBuffer = new Uint8Array(4 + encodedVendorString.length);
let currentView = new DataView(currentBuffer.buffer);
currentView.setUint32(0, encodedVendorString.length, true);
currentBuffer.set(encodedVendorString, 4);
commentHeaderParts.push(currentBuffer);
const writtenTags = new Set<string>();
const addCommentTag = (key: string, value: string) => {
const joined = `${key}=${value}`;
const encoded = textEncoder.encode(joined);
currentBuffer = new Uint8Array(4 + encoded.length);
currentView = new DataView(currentBuffer.buffer);
currentView.setUint32(0, encoded.length, true);
currentBuffer.set(encoded, 4);
commentHeaderParts.push(currentBuffer);
writtenTags.add(key);
};
for (const { key, value } of keyValueIterator(tags)) {
switch (key) {
case 'title': {
addCommentTag('TITLE', value);
}; break;
case 'description': {
addCommentTag('DESCRIPTION', value);
}; break;
case 'artist': {
addCommentTag('ARTIST', value);
}; break;
case 'album': {
addCommentTag('ALBUM', value);
}; break;
case 'albumArtist': {
addCommentTag('ALBUMARTIST', value);
}; break;
case 'genre': {
addCommentTag('GENRE', value);
}; break;
case 'date': {
const rawVersion = tags.raw?.['DATE'] ?? tags.raw?.['date'];
if (rawVersion && typeof rawVersion === 'string') {
addCommentTag('DATE', rawVersion);
} else {
addCommentTag('DATE', value.toISOString().slice(0, 10));
}
}; break;
case 'comment': {
addCommentTag('COMMENT', value);
}; break;
case 'lyrics': {
addCommentTag('LYRICS', value);
}; break;
case 'trackNumber': {
addCommentTag('TRACKNUMBER', value.toString());
}; break;
case 'tracksTotal': {
addCommentTag('TRACKTOTAL', value.toString());
}; break;
case 'discNumber': {
addCommentTag('DISCNUMBER', value.toString());
}; break;
case 'discsTotal': {
addCommentTag('DISCTOTAL', value.toString());
}; break;
case 'images': {
// For example, in .flac, we put the pictures in a different section,
// not in the Vorbis comment header.
if (!writeImages) {
break;
}
for (const image of value) {
// https://datatracker.ietf.org/doc/rfc9639/ Section 8.8
const pictureType = image.kind === 'coverFront' ? 3 : image.kind === 'coverBack' ? 4 : 0;
const encodedMediaType = new Uint8Array(image.mimeType.length);
for (let i = 0; i < image.mimeType.length; i++) {
encodedMediaType[i] = image.mimeType.charCodeAt(i);
}
const encodedDescription = textEncoder.encode(image.description ?? '');
const buffer = new Uint8Array(
4 // Picture type
+ 4 // MIME type length
+ encodedMediaType.length // MIME type
+ 4 // Description length
+ encodedDescription.length // Description
+ 16 // Width, height, color depth, number of colors
+ 4 // Picture data length
+ image.data.length, // Picture data
);
const view = toDataView(buffer);
view.setUint32(0, pictureType, false);
view.setUint32(4, encodedMediaType.length, false);
buffer.set(encodedMediaType, 8);
view.setUint32(8 + encodedMediaType.length, encodedDescription.length, false);
buffer.set(encodedDescription, 12 + encodedMediaType.length);
// Skip a bunch of fields (width, height, color depth, number of colors)
view.setUint32(
28 + encodedMediaType.length + encodedDescription.length, image.data.length, false,
);
buffer.set(
image.data,
32 + encodedMediaType.length + encodedDescription.length,
);
const encoded = bytesToBase64(buffer);
addCommentTag('METADATA_BLOCK_PICTURE', encoded);
}
}; break;
case 'raw': {
// Handled later
}; break;
default: assertNever(key);
}
}
if (tags.raw) {
for (const key in tags.raw) {
const value = tags.raw[key] ?? tags.raw[key.toLowerCase()];
if (key === 'vendor' || value == null || writtenTags.has(key)) {
continue;
}
if (typeof value === 'string') {
addCommentTag(key, value);
}
}
}
const listLengthBuffer = new Uint8Array(4);
toDataView(listLengthBuffer).setUint32(0, writtenTags.size, true);
commentHeaderParts.splice(2, 0, listLengthBuffer); // Insert after the header and vendor section
// Merge all comment header parts into a single buffer
const commentHeaderLength = commentHeaderParts.reduce((a, b) => a + b.length, 0);
const commentHeader = new Uint8Array(commentHeaderLength);
let pos = 0;
for (const part of commentHeaderParts) {
commentHeader.set(part, pos);
pos += part.length;
}
return commentHeader;
};
// ============================================================================
// AC-3 / E-AC-3 Parsing
// Reference: ETSI TS 102 366 V1.4.1
// ============================================================================
/**
* Channel counts indexed by acmod (Table 4.3).
* Does NOT include LFE - add lfeon to get total channel count.
*/
export const AC3_ACMOD_CHANNEL_COUNTS = [2, 1, 2, 3, 3, 4, 4, 5];
export interface Ac3FrameInfo {
/** Sample rate code */
fscod: number;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Bit rate code (0-18, maps to bitrate via Table F.4.1) */
bitRateCode: number;
}
/**
* Parse an AC-3 syncframe to extract BSI (Bit Stream Information) fields.
* Section 4.3
*/
export const parseAc3SyncFrame = (data: Uint8Array): Ac3FrameInfo | null => {
if (data.length < 7) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
bitstream.skipBits(16); // crc1
const fscod = bitstream.readBits(2);
if (fscod === 3) {
return null; // Reserved, invalid
}
const frmsizecod = bitstream.readBits(6);
const bsid = bitstream.readBits(5);
// Verify this is AC-3
if (bsid > 8) {
return null;
}
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
// Skip cmixlev (center downmix level) if three front channels are in use (L, C, R).
if ((acmod & 0x1) !== 0 && acmod !== 0x1) {
bitstream.skipBits(2);
}
// Skip surmixlev (surround downmix level) if surround channels are in use.
if ((acmod & 0x4) !== 0) {
bitstream.skipBits(2);
}
// Skip dsurmod if stereo (acmod === 2)
if (acmod === 0x2) {
bitstream.skipBits(2);
}
const lfeon = bitstream.readBits(1);
const bitRateCode = Math.floor(frmsizecod / 2);
return { fscod, bsid, bsmod, acmod, lfeon, bitRateCode };
};
/**
* AC-3 frame sizes in bytes, indexed by [3 * frmsizecod + fscod].
* fscod: 0=48kHz, 1=44.1kHz, 2=32kHz
* Values are 16-bit words * 2 (to convert to bytes).
* Table 4.13
*/
export const AC3_FRAME_SIZES = [
// frmsizecod, [48kHz, 44.1kHz, 32kHz] in bytes
64 * 2, 69 * 2, 96 * 2,
64 * 2, 70 * 2, 96 * 2,
80 * 2, 87 * 2, 120 * 2,
80 * 2, 88 * 2, 120 * 2,
96 * 2, 104 * 2, 144 * 2,
96 * 2, 105 * 2, 144 * 2,
112 * 2, 121 * 2, 168 * 2,
112 * 2, 122 * 2, 168 * 2,
128 * 2, 139 * 2, 192 * 2,
128 * 2, 140 * 2, 192 * 2,
160 * 2, 174 * 2, 240 * 2,
160 * 2, 175 * 2, 240 * 2,
192 * 2, 208 * 2, 288 * 2,
192 * 2, 209 * 2, 288 * 2,
224 * 2, 243 * 2, 336 * 2,
224 * 2, 244 * 2, 336 * 2,
256 * 2, 278 * 2, 384 * 2,
256 * 2, 279 * 2, 384 * 2,
320 * 2, 348 * 2, 480 * 2,
320 * 2, 349 * 2, 480 * 2,
384 * 2, 417 * 2, 576 * 2,
384 * 2, 418 * 2, 576 * 2,
448 * 2, 487 * 2, 672 * 2,
448 * 2, 488 * 2, 672 * 2,
512 * 2, 557 * 2, 768 * 2,
512 * 2, 558 * 2, 768 * 2,
640 * 2, 696 * 2, 960 * 2,
640 * 2, 697 * 2, 960 * 2,
768 * 2, 835 * 2, 1152 * 2,
768 * 2, 836 * 2, 1152 * 2,
896 * 2, 975 * 2, 1344 * 2,
896 * 2, 976 * 2, 1344 * 2,
1024 * 2, 1114 * 2, 1536 * 2,
1024 * 2, 1115 * 2, 1536 * 2,
1152 * 2, 1253 * 2, 1728 * 2,
1152 * 2, 1254 * 2, 1728 * 2,
1280 * 2, 1393 * 2, 1920 * 2,
1280 * 2, 1394 * 2, 1920 * 2,
];
/** Number of samples per AC-3 syncframe (always 1536) */
export const AC3_SAMPLES_PER_FRAME = 1536;
/**
* AC-3 registration_descriptor for MPEG-TS.
* Section A.2.3
*/
export const AC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x41, 0x43, 0x2d, 0x33]);
/** E-AC-3 registration_descriptor for MPEG-TS/ */
export const EAC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x45, 0x41, 0x43, 0x33]);
/** Number of audio blocks per syncframe, indexed by numblkscod */
export const EAC3_NUMBLKS_TABLE = [1, 2, 3, 6];
/**
* E-AC-3 independent substream info.
* Each independent substream represents a separate audio program.
*/
export interface Eac3SubstreamInfo {
/** Sample rate code */
fscod: number;
/** Sample rate code 2 (ATSC A/52:2018) */
fscod2: number | null;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Number of dependent substreams */
numDepSub: number;
/** Channel locations for dependent substreams */
chanLoc: number;
}
/**
* E-AC-3 decoder configuration (dec3 box contents).
*/
export interface Eac3FrameInfo {
/** Data rate in kbps */
dataRate: number;
/** Independent substreams */
substreams: Eac3SubstreamInfo[];
}
/**
* Parse an E-AC-3 syncframe to extract BSI fields.
* Section E.1.2
*/
export const parseEac3SyncFrame = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 6) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
const strmtyp = bitstream.readBits(2);
bitstream.skipBits(3); // substreamid
// Only parse independent substreams (strmtyp 0 or 2)
if (strmtyp !== 0 && strmtyp !== 2) {
return null;
}
const frmsiz = bitstream.readBits(11);
const fscod = bitstream.readBits(2);
let fscod2 = 0;
let numblkscod: number;
if (fscod === 3) {
// fscod2 enables reduced sample rates (24/22.05/16 kHz) per ATSC A/52:2018
fscod2 = bitstream.readBits(2);
numblkscod = 3; // Implicitly 6 blocks when fscod=3
} else {
numblkscod = bitstream.readBits(2);
}
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
const bsid = bitstream.readBits(5);
// Verify this is E-AC-3
if (bsid < 11 || bsid > 16) {
return null;
}
// Calculate data rate: ((frmsiz + 1) * fs) / (numblks * 16)
const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;
let fs: number;
if (fscod < 3) {
fs = AC3_SAMPLE_RATES[fscod]! / 1000;
} else {
fs = EAC3_REDUCED_SAMPLE_RATES[fscod2]! / 1000;
}
const dataRate = Math.round(((frmsiz + 1) * fs) / (numblks * 16));
// These fields require parsing beyond the first frame.
// Defaults are correct for almost all content.
const bsmod = 0;
const numDepSub = 0;
const chanLoc = 0;
const substream: Eac3SubstreamInfo = {
fscod,
fscod2,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
};
return {
dataRate,
substreams: [substream],
};
};
/**
* Parse a dec3 box to extract E-AC-3 parameters.
* Section F.6
*/
export const parseEac3Config = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 2) {
return null;
}
const bitstream = new Bitstream(data);
const dataRate = bitstream.readBits(13);
const numIndSub = bitstream.readBits(3);
const substreams: Eac3SubstreamInfo[] = [];
for (let i = 0; i <= numIndSub; i++) {
// Check we have enough data for this substream
// Each substream needs at least 24 bits (3 bytes) without dependent subs
if (Math.ceil(bitstream.pos / 8) + 3 > data.length) {
break;
}
const fscod = bitstream.readBits(2);
const bsid = bitstream.readBits(5);
bitstream.skipBits(1); // reserved
bitstream.skipBits(1); // asvc
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
bitstream.skipBits(3); // reserved
const numDepSub = bitstream.readBits(4);
let chanLoc = 0;
if (numDepSub > 0) {
chanLoc = bitstream.readBits(9);
} else {
bitstream.skipBits(1); // reserved
}
substreams.push({
fscod,
fscod2: null,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
});
}
if (substreams.length === 0) {
return null;
}
return { dataRate, substreams };
};
/**
* Get sample rate from E-AC-3 config.
* See ATSC A/52:2018 for handling fscod2.
*/
export const getEac3SampleRate = (config: Eac3FrameInfo): number | null => {
const sub = config.substreams[0];
assert(sub);
if (sub.fscod < 3) {
return AC3_SAMPLE_RATES[sub.fscod]!;
} else if (sub.fscod2 !== null && sub.fscod2 < 3) {
return EAC3_REDUCED_SAMPLE_RATES[sub.fscod2]!;
}
return null;
};
/**
* Get channel count from E-AC-3 config (first independent substream only).
*/
export const getEac3ChannelCount = (config: Eac3FrameInfo): number => {
const sub = config.substreams[0];
assert(sub);
let channels = AC3_ACMOD_CHANNEL_COUNTS[sub.acmod]! + sub.lfeon;
// Add channels from dependent substreams
if (sub.numDepSub > 0) {
const CHAN_LOC_COUNTS = [2, 2, 1, 1, 2, 2, 2, 1, 1];
for (let bit = 0; bit < 9; bit++) {
if (sub.chanLoc & (1 << (8 - bit))) {
channels += CHAN_LOC_COUNTS[bit]!;
}
}
}
return channels;
};
// ============================================================================
// DTS Parsing
// Reference: ETSI TS 102 114 V1.6.1
// ============================================================================
/** Core substream sync word, in the 16-bit big-endian packing that the registry mandates. */
export const DTS_CORE_SYNC_WORD = 0x7ffe8001;
/** Extension substream sync word. Section 7.4.1 */
export const DTS_EXSS_SYNC_WORD = 0x64582025;
/** The core frame header never reaches beyond this many bytes. */
export const DTS_CORE_FRAME_HEADER_SIZE = 18;
/** An extension substream always declares its own size within this many bytes. */
export const DTS_EXSS_HEADER_PREFIX_SIZE = 10;
/** The largest nuExtSSHeaderSize can get, and therefore how far the asset descriptors can reach. */
export const DTS_EXSS_MAX_HEADER_SIZE = 4096;
/** Number of PCM samples in one core PCM block; the core codes its length as a count of these. */
export const DTS_PCM_BLOCK_SAMPLES = 32;
/** Size of the DTSSpecificBox (ddts) payload in bytes. */
export const DTS_SPECIFIC_BOX_SIZE = 20;
/** Number of PCM blocks that a core frame's length must be a multiple of. */
const DTS_SUBBAND_SAMPLES = 8;
/** Core sample rates indexed by SFREQ. Zeroes mark invalid codes. Table 5-4 */
const DTS_CORE_SAMPLE_RATES = [
0, 8000, 16000, 32000, 0, 0, 11025, 22050,
44100, 0, 0, 12000, 24000, 48000, 96000, 192000,
];
/**
* Core bit rates in bps indexed by RATE, where a zero means the code isn't a constant rate. Table 5-7
*
* Note that FFmpeg's ff_dca_bit_rates has 896000 where the spec has 960, and defines rates for codes 25 to 28
* which this revision of the spec calls invalid. We keep the latter, since they cost nothing and some content
* predating the spec revision uses them.
*/
const DTS_CORE_BIT_RATES = [
32000, 56000, 64000, 96000, 112000, 128000, 192000, 224000,
256000, 320000, 384000, 448000, 512000, 576000, 640000, 768000,
960000, 1024000, 1152000, 1280000, 1344000, 1408000, 1411200, 1472000,
1536000, 1920000, 2048000, 3072000, 3840000, 0, 0, 0,
];
/** Source PCM resolutions in bits indexed by PCMR. Zeroes mark invalid codes. */
const DTS_PCM_RESOLUTIONS = [16, 16, 20, 20, 0, 24, 24, 0];
/** Channel counts indexed by AMODE, not counting LFE. */
const DTS_AMODE_CHANNEL_COUNTS = [1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8];
/**
* Speaker layout masks indexed by AMODE, expressed with the same bits as `ChannelLayout` in the DTSSpecificBox
* and `nuSpkrActivityMask` in an extension substream asset descriptor.
*/
const DTS_AMODE_CHANNEL_LAYOUTS = [
0x0001, 0x0002, 0x0002, 0x0002, 0x0002, 0x0003, 0x0012, 0x0013,
0x0006, 0x0007, 0x0206, 0x0143, 0x0053, 0x0207, 0x0246, 0x0217,
];
/** The LFE1 speaker bit in a channel layout mask. */
const DTS_CHANNEL_LAYOUT_LFE1 = 0x0008;
/** The channel layout bits that stand for a pair of speakers rather than a single one. */
const DTS_CHANNEL_LAYOUT_PAIR_MASK = 0xae66;
/** Reference clock rates indexed by nuRefClockCode. The last code is unused. Table 7-3 */
const DTS_EXSS_REF_CLOCKS = [32000, 44100, 48000, 0];
/** Sample rates used by extension substream assets, indexed by nuMaxSampleRate. */
const DTS_EXSS_SAMPLE_RATES = [
8000, 16000, 32000, 64000, 128000, 22050, 44100, 88200,
176400, 352800, 12000, 24000, 48000, 96000, 192000, 384000,
];
/** Frame durations that the DTSSpecificBox can express, indexed by FrameDuration. */
const DTS_SPECIFIC_BOX_FRAME_DURATIONS = [512, 1024, 2048, 4096];
export type DtsCoreFrameInfo = {
/** Size of the core substream frame in bytes */
frameSize: number;
sampleRate: number;
numberOfChannels: number;
/** Number of PCM samples the frame decodes to */
sampleCount: number;
/** Speaker layout mask */
channelLayout: number;
/** Audio channel arrangement (AMODE) */
amode: number;
/** Whether an LFE channel is present */
lfePresent: boolean;
/** Constant bit rate in bps, or 0 when the stream doesn't run at one */
bitRate: number;
/** Source PCM resolution in bits */
pcmResolution: number;
};
export type DtsExssAssetInfo = {
sampleRate: number;
numberOfChannels: number;
/** Number of PCM samples the frame decodes to */
sampleCount: number;
/** Speaker layout mask, or 0 when the asset doesn't declare one */
channelLayout: number;
/** Source PCM resolution in bits */
pcmResolution: number;
};
export type DtsExssInfo = {
/** Size of the extension substream in bytes */
frameSize: number;
/** Null when this substream omits the static fields that describe the stream */
asset: DtsExssAssetInfo | null;
};
export type DtsFrameInfo = {
/** Size of the entire frame in bytes, core substream plus any extension substreams */
frameSize: number;
sampleRate: number;
numberOfChannels: number;
/** Number of PCM samples the frame decodes to */
sampleCount: number;
/** Speaker layout mask */
channelLayout: number;
/** Source PCM resolution in bits */
pcmResolution: number;
/** Constant bit rate in bps, or 0 when the stream doesn't run at one */
bitRate: number;
/** The leading core substream, or null for extension-only streams such as DTS Express */
core: DtsCoreFrameInfo | null;
/** Whether the frame carries extension substreams on top of the core */
hasExtensions: boolean;
};
/**
* Parse one complete DTS frame, being a core substream frame followed by any number of extension substreams,
* or an extension substream on its own. Section 5 and Section 7.4.1
*/
export const parseDtsFrame = (data: Uint8Array): DtsFrameInfo | null => {
const core = parseDtsCoreFrameHeader(data);
const view = toDataView(data);
// The core substream is padded out to a 4-byte boundary before the first extension substream starts
let offset = core ? Math.ceil(core.frameSize / 4) * 4 : 0;
let firstExss: DtsExssInfo | null = null;
while (offset + 4 <= data.length && view.getUint32(offset) === DTS_EXSS_SYNC_WORD) {
const exss = parseDtsExssHeader(data.subarray(offset));
if (!exss) {
break;
}
firstExss ??= exss;
offset += exss.frameSize;
}
if (core) {
// The core describes what every DTS decoder can play back; the extension substreams only build on top of
// it, so the core's parameters are the ones we report.
return {
frameSize: firstExss ? offset : core.frameSize,
sampleRate: core.sampleRate,
numberOfChannels: core.numberOfChannels,
sampleCount: core.sampleCount,
channelLayout: core.channelLayout,
pcmResolution: core.pcmResolution,
bitRate: core.bitRate,
core,
hasExtensions: firstExss !== null,
};
}
if (!firstExss?.asset) {
return null;
}
const { asset } = firstExss;
return {
frameSize: offset,
sampleRate: asset.sampleRate,
numberOfChannels: asset.numberOfChannels,
sampleCount: asset.sampleCount,
channelLayout: asset.channelLayout,
pcmResolution: asset.pcmResolution,
bitRate: 0,
core: null,
hasExtensions: true,
};
};
/**
* Works out which four-character code describes a packet, or null when the packet doesn't say. Telling 'dtsl'
* from 'dtse' would mean working out whether the asset holds XLL or LBR data, which sits behind the speaker
* remapping and mixing metadata deep in the asset descriptor, so we don't do it.
*/
export const extractDtsFourCcFromPacket = (data: Uint8Array): DtsFourCc | null => {
const frameInfo = parseDtsFrame(data);
if (!frameInfo?.core) {
return null;
}
return frameInfo.hasExtensions ? 'dtsh' : 'dtsc';
};
/** Parse the header of a core substream frame. Section 5.3 */
export const parseDtsCoreFrameHeader = (data: Uint8Array): DtsCoreFrameInfo | null => {
if (data.length < DTS_CORE_FRAME_HEADER_SIZE) {
return null;
}
if (data[0] !== 0x7f || data[1] !== 0xfe || data[2] !== 0x80 || data[3] !== 0x01) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(32); // SYNC
bitstream.skipBits(1); // FTYPE
// Terminating frames carry fewer samples than a full PCM block; we don't handle those
if (bitstream.readBits(5) !== DTS_PCM_BLOCK_SAMPLES - 1) {
return null;
}
const cpf = bitstream.readBits(1);
const npcmblocks = bitstream.readBits(7) + 1;
if (npcmblocks % DTS_SUBBAND_SAMPLES !== 0) {
return null;
}
const frameSize = bitstream.readBits(14) + 1;
if (frameSize < 96) {
return null;
}
const amode = bitstream.readBits(6);
if (amode >= DTS_AMODE_CHANNEL_COUNTS.length) {
return null;
}
const sampleRate = DTS_CORE_SAMPLE_RATES[bitstream.readBits(4)]!;
if (sampleRate === 0) {
return null;
}
const bitRate = DTS_CORE_BIT_RATES[bitstream.readBits(5)]!;
if (bitstream.readBits(1) !== 0) {
return null; // A reserved bit that must be zero, so a cheap way to reject false sync words
}
bitstream.skipBits(1 + 1 + 1 + 1); // DYNF, TIMEF, AUXF, HDCD
bitstream.skipBits(3 + 1 + 1); // EXT_AUDIO_ID, EXT_AUDIO, ASPF
const lff = bitstream.readBits(2);
if (lff === 3) {
return null;
}
bitstream.skipBits(1); // HFLAG
if (cpf) {
bitstream.skipBits(16); // HCRC
}
bitstream.skipBits(1 + 4 + 2); // FILTS, VERNUM, CHIST
const pcmResolution = DTS_PCM_RESOLUTIONS[bitstream.readBits(3)]!;
if (pcmResolution === 0) {
return null;
}
const lfePresent = lff !== 0;
return {
frameSize,
sampleRate,
numberOfChannels: DTS_AMODE_CHANNEL_COUNTS[amode]! + (lfePresent ? 1 : 0),
sampleCount: npcmblocks * DTS_PCM_BLOCK_SAMPLES,
channelLayout: DTS_AMODE_CHANNEL_LAYOUTS[amode]! | (lfePresent ? DTS_CHANNEL_LAYOUT_LFE1 : 0),
amode,
lfePresent,
bitRate,
pcmResolution,
};
};
/** Parse the header of an extension substream, along with its first audio asset descriptor. Section 7.4.1 */
export const parseDtsExssHeader = (data: Uint8Array): DtsExssInfo | null => {
if (data.length < DTS_EXSS_HEADER_PREFIX_SIZE) {
return null;
}
if (data[0] !== 0x64 || data[1] !== 0x58 || data[2] !== 0x20 || data[3] !== 0x25) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(32); // SYNC
bitstream.skipBits(8); // nuUserDefinedBits
const extSsIndex = bitstream.readBits(2);
const wideHeader = bitstream.readBits(1);
const headerSizeBits = 8 + 4 * wideHeader;
const frameSizeBits = 16 + 4 * wideHeader;
bitstream.skipBits(headerSizeBits); // nuExtSSHeaderSize
const frameSize = bitstream.readBits(frameSizeBits) + 1;
// Everything past this point can run off the end of what we were given, in which case the Bitstream keeps
// handing out zeroes; the bounds check further down catches that
const incomplete: DtsExssInfo = { frameSize, asset: null };
if (!bitstream.readBits(1)) { // bStaticFieldsPresent
return incomplete;
}
const refClock = DTS_EXSS_REF_CLOCKS[bitstream.readBits(2)]!; // nuRefClockCode
// The frame duration is a count of reference clock cycles, not of samples
const frameDurationCycles = 512 * (bitstream.readBits(3) + 1); // nuExSSFrameDurationCode
if (bitstream.readBits(1)) { // bTimeStampFlag
bitstream.skipBits(32 + 4); // nuTimeStamp, nLSB
}
const numAudioPresentations = bitstream.readBits(3) + 1;
const numAssets = bitstream.readBits(3) + 1;
const activeExssMasks: number[] = [];
for (let i = 0; i < numAudioPresentations; i++) {
activeExssMasks.push(bitstream.readBits(extSsIndex + 1));
}
for (const mask of activeExssMasks) {
bitstream.skipBits(8 * popcount(mask)); // nuActiveAssetMask
}
if (bitstream.readBits(1)) { // bMixMetadataEnbl
bitstream.skipBits(2); // nuMixMetadataAdjLevel
const spkrMaskBits = (bitstream.readBits(2) + 1) << 2;
const numMixOutConfigs = bitstream.readBits(2) + 1;
bitstream.skipBits(numMixOutConfigs * spkrMaskBits); // nuMixOutChMask
}
for (let i = 0; i < numAssets; i++) {
bitstream.skipBits(frameSizeBits); // nuAssetFsize
}
// From here on we're inside the first audio asset descriptor
bitstream.skipBits(9); // nuAssetDescriptFsize
bitstream.skipBits(3); // nuAssetIndex
if (bitstream.readBits(1)) { // bAssetTypeDescrPresent
bitstream.skipBits(4); // nuAssetTypeDescriptor
}
if (bitstream.readBits(1)) { // bLanguageDescrPresent
bitstream.skipBits(24); // LanguageDescriptor
}
if (bitstream.readBits(1)) { // bInfoTextPresent
bitstream.skipBits(8 * (bitstream.readBits(10) + 1)); // nuInfoTextByteSize, InfoTextString
}
const pcmResolution = bitstream.readBits(5) + 1;
const sampleRate = DTS_EXSS_SAMPLE_RATES[bitstream.readBits(4)]!;
const numberOfChannels = bitstream.readBits(8) + 1;
let channelLayout = 0;
if (bitstream.readBits(1)) { // bOne2OneMapChannels2Speakers
if (numberOfChannels > 2) {
bitstream.skipBits(1); // bEmbeddedStereoFlag
}
if (numberOfChannels > 6) {
bitstream.skipBits(1); // bEmbeddedSixChFlag
}
if (bitstream.readBits(1)) { // bSpkrMaskEnabled
const spkrMaskBits = (bitstream.readBits(2) + 1) << 2;
channelLayout = bitstream.readBits(spkrMaskBits); // nuSpkrActivityMask
}
}
if (refClock === 0 || bitstream.getBitsLeft() < 0) {
return incomplete;
}
return {
frameSize,
asset: {
sampleRate,
numberOfChannels,
sampleCount: Math.round(frameDurationCycles * sampleRate / refClock),
channelLayout,
pcmResolution,
},
};
};
export type DtsSpecificBoxInfo = {
sampleRate: number;
maxBitrate: number;
avgBitrate: number;
pcmSampleDepth: number;
/** Number of PCM samples one frame decodes to */
sampleCount: number;
/** Speaker layout mask, or 0 when the box doesn't declare one */
channelLayout: number;
/** Null when the box carries nothing we can derive a channel count from */
numberOfChannels: number | null;
};
/** Parse a DTSSpecificBox (ddts). */
export const parseDtsSpecificBox = (data: Uint8Array): DtsSpecificBoxInfo | null => {
if (data.length < DTS_SPECIFIC_BOX_SIZE) {
return null;
}
const view = toDataView(data);
const sampleRate = view.getUint32(0);
if (sampleRate === 0) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.seekToByte(13);
const frameDuration = bitstream.readBits(2);
bitstream.skipBits(5); // StreamConstruction
const coreLfePresent = bitstream.readBits(1);
const coreLayout = bitstream.readBits(6);
bitstream.skipBits(14); // CoreSize
bitstream.skipBits(1); // StereoDownmix
bitstream.skipBits(3); // RepresentationType
const channelLayout = bitstream.readBits(16);
let numberOfChannels: number | null = null;
if (channelLayout !== 0) {
numberOfChannels = getDtsChannelCount(channelLayout);
} else if (coreLayout < DTS_AMODE_CHANNEL_COUNTS.length) {
numberOfChannels = DTS_AMODE_CHANNEL_COUNTS[coreLayout]! + coreLfePresent;
}
return {
sampleRate,
maxBitrate: view.getUint32(4),
avgBitrate: view.getUint32(8),
pcmSampleDepth: data[12]!,
sampleCount: DTS_SPECIFIC_BOX_FRAME_DURATIONS[frameDuration]!,
channelLayout,
numberOfChannels,
};
};
/** Build the payload of a DTSSpecificBox (ddts) from a frame of the stream it describes. */
export const buildDtsSpecificBox = (frameInfo: DtsFrameInfo) => {
const bytes = new Uint8Array(DTS_SPECIFIC_BOX_SIZE);
const view = toDataView(bytes);
view.setUint32(0, frameInfo.sampleRate);
view.setUint32(4, frameInfo.bitRate);
view.setUint32(8, frameInfo.bitRate);
bytes[12] = frameInfo.pcmResolution;
// The spec only defines codes for streams built out of a known set of substreams. Anything else is required
// to be signaled as 0, meaning the construction is left unspecified.
const streamConstruction = frameInfo.core && !frameInfo.hasExtensions ? 1 : 0;
const bitstream = new Bitstream(bytes);
bitstream.seekToByte(13);
bitstream.writeBits(2, Math.max(DTS_SPECIFIC_BOX_FRAME_DURATIONS.indexOf(frameInfo.sampleCount), 0));
bitstream.writeBits(5, streamConstruction);
bitstream.writeBits(1, frameInfo.core?.lfePresent ? 1 : 0);
bitstream.writeBits(6, frameInfo.core?.amode ?? 0);
bitstream.writeBits(14, frameInfo.core ? frameInfo.core.frameSize - 1 : 0);
bitstream.writeBits(1, 0); // StereoDownmix
bitstream.writeBits(3, 0); // RepresentationType
bitstream.writeBits(16, frameInfo.channelLayout);
bitstream.writeBits(1, 0); // MultiAssetFlag
bitstream.writeBits(1, 0); // LBRDurationMod
bitstream.writeBits(1, 0); // ReservedBoxPresent
bitstream.writeBits(5, 0); // Reserved
return bytes;
};
/** Count the channels in a DTS speaker layout mask, where some bits stand for a pair of speakers. */
const getDtsChannelCount = (channelLayout: number) => {
return popcount(channelLayout) + popcount(channelLayout & DTS_CHANNEL_LAYOUT_PAIR_MASK);
};