Add ProRes codec and allow demuxing & muxing

This commit is contained in:
Vanilagy
2025-12-10 09:52:54 +01:00
parent 49af8d5435
commit 0dd53ff1f7
9 changed files with 193 additions and 34 deletions
+45 -5
View File
@@ -18,6 +18,7 @@ import {
MATRIX_COEFFICIENTS_MAP,
TRANSFER_CHARACTERISTICS_MAP,
assert,
assertNever,
bytesToHexString,
isAllowSharedBufferSource,
last,
@@ -37,6 +38,7 @@ export const VIDEO_CODECS = [
'vp9',
'av1',
'vp8',
'prores',
] as const;
/**
* List of known PCM (uncompressed) audio codecs, ordered by encoding preference.
@@ -212,6 +214,17 @@ const AV1_LEVEL_TABLE = [
const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00';
const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0';
export const PRORES_FOURCCS = [
'ap4x',
'ap4h',
'apch',
'apcn',
'apcs',
'apco',
'aprh',
'aprn',
];
export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number, bitrate: number) => {
if (codec === 'avc') {
const profileIndication = 0x64; // High Profile
@@ -271,10 +284,13 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
const bitDepth = '08'; // 8-bit
return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
} else if (codec === 'prores') {
return 'apr1.apch';
} else {
assertNever(codec);
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new TypeError(`Unhandled codec '${codec}'.`);
throw new TypeError(`Unhandled codec '${String(codec)}'.`);
};
export const generateVp9CodecConfigurationFromCodecString = (codecString: string) => {
@@ -342,8 +358,18 @@ export const extractVideoCodecString = (trackInfo: {
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
proresFormat: string | null;
}) => {
const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
const {
codec,
codecDescription,
colorSpace,
avcCodecInfo,
hevcCodecInfo,
vp9CodecInfo,
av1CodecInfo,
proresFormat,
} = trackInfo;
if (codec === 'avc') {
assert(trackInfo.avcType !== null);
@@ -501,6 +527,10 @@ export const extractVideoCodecString = (trackInfo: {
}
return string;
} else if (codec === 'prores') {
return `apr1.${proresFormat ?? 'apch'}`;
} else if (codec !== null) {
assertNever(codec);
}
throw new TypeError(`Unhandled codec '${codec}'.`);
@@ -786,7 +816,7 @@ export const getAudioEncoderConfigExtension = (codec: AudioCodec) => {
return {};
};
const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01'];
const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01', 'apr1'];
const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/;
const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
@@ -811,7 +841,7 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
+ ' the WebCodecs Codec Registry.',
+ ' the WebCodecs Codec Registry.', // todo?
);
}
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
@@ -922,6 +952,16 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
+ ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.',
);
}
} else if (metadata.decoderConfig.codec.startsWith('apr1')) {
// ProRes-specific validation
const parts = metadata.decoderConfig.codec.split('.');
if (parts.length !== 2 || parts[0] !== 'apr1' || !PRORES_FOURCCS.includes(parts[1]!)) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string for ProRes must be a valid ProRes codec'
+ ' string as specified in the Mediabunny Codec Registry.',
);
}
}
};