mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
3672 lines
106 KiB
TypeScript
3672 lines
106 KiB
TypeScript
/*!
|
||
* 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);
|
||
};
|