Add ProRes encode and decode support to @mediabunny/server

This commit is contained in:
Vanilagy
2026-06-30 21:40:58 +02:00
parent b4ef20cd36
commit 7d89c8de2d
18 changed files with 383 additions and 27 deletions
+35 -2
View File
@@ -227,7 +227,23 @@ export const PRORES_FOURCCS = [
] as const;
export type ProresFourCc = typeof PRORES_FOURCCS[number];
export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number, bitrate: number) => {
// Target data rates of the ProRes profiles at 1920x1080 ~30fps, as published by Apple
const PRORES_PROFILE_TARGET_BITRATES: { fourCc: ProresFourCc; bitrate: number; alpha: boolean }[] = [
{ fourCc: 'apco', bitrate: 45_000_000, alpha: false }, // 422 Proxy
{ fourCc: 'apcs', bitrate: 102_000_000, alpha: false }, // 422 LT
{ fourCc: 'apcn', bitrate: 147_000_000, alpha: false }, // 422 Standard
{ fourCc: 'apch', bitrate: 220_000_000, alpha: false }, // 422 HQ
{ fourCc: 'ap4h', bitrate: 330_000_000, alpha: true }, // 4444
{ fourCc: 'ap4x', bitrate: 500_000_000, alpha: true }, // 4444 XQ
];
export const buildVideoCodecString = (
codec: VideoCodec,
width: number,
height: number,
bitrate: number,
alpha: boolean,
) => {
if (codec === 'avc') {
const profileIndication = 0x64; // High Profile
const totalMacroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
@@ -287,7 +303,22 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
} else if (codec === 'prores') {
return 'apch';
const referencePixels = 1920 * 1080;
const scaleFactor = Math.pow((width * height) / referencePixels, 0.95);
const candidates = PRORES_PROFILE_TARGET_BITRATES.filter(x => x.alpha === alpha);
let bestFourCc = candidates[0]!.fourCc;
let smallestDifference = Infinity;
for (const { fourCc, bitrate: targetBitrate } of candidates) {
const difference = Math.abs(targetBitrate * scaleFactor - bitrate);
if (difference < smallestDifference) {
smallestDifference = difference;
bestFourCc = fourCc;
}
}
return bestFourCc;
} else {
assertNever(codec);
}
@@ -700,6 +731,8 @@ export const inferCodecFromCodecString = (codecString: string): MediaCodec | nul
return 'vp9';
} else if (codecString.startsWith('av01')) {
return 'av1';
} else if ((PRORES_FOURCCS as readonly string[]).includes(codecString)) {
return 'prores';
}
// Audio codecs
+1 -1
View File
@@ -140,7 +140,7 @@ export const canDecodeVideo = async (
...options,
codedWidth: options.codedWidth ?? 1280,
codedHeight: options.codedHeight ?? 720,
codec: options.codec ?? buildVideoCodecString(codec, 1280, 720, 1e6),
codec: options.codec ?? buildVideoCodecString(codec, 1280, 720, 1e6, false),
};
resolvedOptions.description ??= guessDescriptionForVideo(resolvedOptions);
+4 -2
View File
@@ -251,8 +251,9 @@ export type VideoEncodingAdditionalOptions = {
* What to do with alpha data contained in the video samples.
*
* - `'discard'` (default): Only the samples' color data is kept; the video is opaque.
* - `'keep'`: The samples' alpha data is also encoded as side data. Make sure to pair this mode with a container
* format that supports transparency (such as WebM or Matroska).
* - `'keep'`: The samples' alpha data is also encoded. Depending on the codec, the alpha may be emitted as packet
* side data or in-band alongside the main packet. For codecs that emit alpha side data, such as VP9, make sure to
* pair this mode with a container format that supports transparency (such as WebM or Matroska).
*/
alpha?: 'discard' | 'keep';
/** Configures the bitrate mode; defaults to `'variable'`. */
@@ -344,6 +345,7 @@ export const buildVideoEncoderConfig = (options: {
options.width,
options.height,
resolvedBitrate,
options.alpha === 'keep',
),
width: options.width,
height: options.height,