mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 19:33:51 +02:00
Migrate all codec data parsing logic into new file
This commit is contained in:
+7
-451
@@ -1,4 +1,9 @@
|
||||
import { AvcDecoderConfigurationRecord, HevcDecoderConfigurationRecord } from './avc';
|
||||
import {
|
||||
Av1CodecInfo,
|
||||
AvcDecoderConfigurationRecord,
|
||||
HevcDecoderConfigurationRecord,
|
||||
Vp9CodecInfo,
|
||||
} from './codec-data';
|
||||
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
|
||||
import {
|
||||
Bitstream,
|
||||
@@ -141,7 +146,7 @@ const HEVC_LEVEL_TABLE = [
|
||||
];
|
||||
|
||||
// https://en.wikipedia.org/wiki/VP9
|
||||
const VP9_LEVEL_TABLE = [
|
||||
export const VP9_LEVEL_TABLE = [
|
||||
{ maxPictureSize: 36864, maxBitrate: 200000, level: 10 }, // Level 1
|
||||
{ maxPictureSize: 73728, maxBitrate: 800000, level: 11 }, // Level 1.1
|
||||
{ maxPictureSize: 122880, maxBitrate: 1800000, level: 20 }, // Level 2
|
||||
@@ -308,28 +313,6 @@ export const generateAv1CodecConfigurationFromCodecString = (codecString: string
|
||||
return [firstByte, secondByte, thirdByte, fourthByte];
|
||||
};
|
||||
|
||||
export type Vp9CodecInfo = {
|
||||
profile: number;
|
||||
level: number;
|
||||
bitDepth: number;
|
||||
chromaSubsampling: number;
|
||||
videoFullRangeFlag: number;
|
||||
colourPrimaries: number;
|
||||
transferCharacteristics: number;
|
||||
matrixCoefficients: number;
|
||||
};
|
||||
|
||||
export type Av1CodecInfo = {
|
||||
profile: number;
|
||||
level: number;
|
||||
tier: number;
|
||||
bitDepth: number;
|
||||
monochrome: number;
|
||||
chromaSubsamplingX: number;
|
||||
chromaSubsamplingY: number;
|
||||
chromaSamplePosition: number;
|
||||
};
|
||||
|
||||
export const extractVideoCodecString = (trackInfo: {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -504,383 +487,6 @@ export const extractVideoCodecString = (trackInfo: {
|
||||
throw new TypeError(`Unhandled codec '${codec}'.`);
|
||||
};
|
||||
|
||||
export const extractVp9CodecInfoFromFrame = (
|
||||
frame: 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
|
||||
|
||||
// Handle superframe
|
||||
const lastByte = frame[frame.length - 1];
|
||||
if (lastByte && (lastByte & 0xe0) === 0xc0) { // Is superframe
|
||||
const bytesPerFrameSize = ((lastByte & 0x18) >> 3) + 1;
|
||||
const numFrames = (lastByte & 0x07) + 1;
|
||||
const indexSize = 2 + numFrames * bytesPerFrameSize;
|
||||
|
||||
// Verify matching marker bytes
|
||||
if (frame[frame.length - indexSize] !== lastByte) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get first frame size
|
||||
let frameSize = 0;
|
||||
const offset = frame.length - indexSize + 1;
|
||||
|
||||
for (let i = 0; i < bytesPerFrameSize; i++) {
|
||||
if (!frame[offset + i]) return null;
|
||||
frameSize |= frame[offset + i]! << (8 * i);
|
||||
}
|
||||
|
||||
frame = frame.subarray(0, frameSize);
|
||||
}
|
||||
|
||||
const bitstream = new Bitstream(frame);
|
||||
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 extractAv1CodecInfoFromFrame = (
|
||||
data: Uint8Array,
|
||||
): Av1CodecInfo | null => {
|
||||
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf
|
||||
|
||||
const bitstream = new Bitstream(data);
|
||||
|
||||
const readLeb128 = (): number | null => {
|
||||
let value = 0;
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const byte = bitstream.readAlignedByte();
|
||||
if (byte === undefined) return 0;
|
||||
|
||||
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
|
||||
const obuHeader = bitstream.readBits(8);
|
||||
|
||||
const obuType = (obuHeader >> 3) & 0xf;
|
||||
const obuExtension = (obuHeader >> 2) & 0x1;
|
||||
const obuHasSizeField = (obuHeader >> 1) & 0x1;
|
||||
|
||||
// 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 null; // It was invalid
|
||||
obuSize = obuSizeValue;
|
||||
} else {
|
||||
// Calculate remaining bits and convert to bytes, rounding down
|
||||
obuSize = Math.floor(bitstream.getBitsLeft() / 8);
|
||||
}
|
||||
|
||||
// We're only interested in Sequence Header OBU (type 1)
|
||||
if (obuType === 1) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// Move to next OBU
|
||||
// The OBU size is in bytes, so skip that many bytes.
|
||||
bitstream.skipBits(obuSize * 8);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => {
|
||||
if (codec === 'aac') {
|
||||
// If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS
|
||||
@@ -954,18 +560,14 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null) => {
|
||||
|
||||
const bitstream = new Bitstream(bytes);
|
||||
|
||||
// 1) Audio Object Type (5 bits)
|
||||
let objectType = bitstream.readBits(5);
|
||||
if (objectType === 31) {
|
||||
// (Escape value) -> 6 bits + 32
|
||||
objectType = 32 + bitstream.readBits(6);
|
||||
}
|
||||
|
||||
// 2) Sampling Frequency Index (4 bits)
|
||||
const frequencyIndex = bitstream.readBits(4);
|
||||
let sampleRate: number | null = null;
|
||||
if (frequencyIndex === 15) {
|
||||
// Explicit sample rate (24 bits)
|
||||
sampleRate = bitstream.readBits(24);
|
||||
} else {
|
||||
const freqTable = [
|
||||
@@ -977,7 +579,6 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Channel Configuration (4 bits)
|
||||
const channelConfiguration = bitstream.readBits(4);
|
||||
let numberOfChannels: number | null = null;
|
||||
if (channelConfiguration >= 1 && channelConfiguration <= 7) {
|
||||
@@ -1002,53 +603,8 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | 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,
|
||||
};
|
||||
};
|
||||
|
||||
export const OPUS_INTERNAL_SAMPLE_RATE = 48000;
|
||||
|
||||
// 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;
|
||||
|
||||
return {
|
||||
durationInSamples: OPUS_FRAME_DURATION_TABLE[config]!,
|
||||
};
|
||||
};
|
||||
|
||||
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
|
||||
|
||||
export const parsePcmCodec = (codec: PcmAudioCodec) => {
|
||||
|
||||
Reference in New Issue
Block a user