Files
hyperframes/packages/engine/src/utils/evenDimensions.ts
T

56 lines
2.4 KiB
TypeScript

/**
* Even-dimension normalization for chroma-subsampled encodes.
*
* libx264 / libx265 with 4:2:0 chroma subsampling (yuv420p, yuv420p10le)
* require both width and height to be even. An odd output dimension makes the
* encoder abort before writing a single packet:
*
* [libx264] height not divisible by 2 (1080x723)
* Error while opening encoder ... Invalid argument
*
* A composition with an odd data-width / data-height (e.g. a custom 3:1 canvas
* at 1080x723) therefore fails to encode to H.264. The fix pads the odd
* dimension up by a single pixel inside the encode filter chain, so the
* encoder always receives even dimensions. Padding (not scaling) means content
* is never resampled — at most one transparent/black row or column is added at
* the bottom-right edge.
*/
// `pad=ceil(iw/2)*2:ceil(ih/2)*2` rounds each dimension UP to the next even
// value (a no-op when already even) and keeps content at the top-left (x=0,y=0
// default), so nothing shifts. iw/ih are evaluated by FFmpeg at runtime, so
// this works whether or not the caller knows the frame size up front.
const EVEN_DIMENSION_PAD = "pad=ceil(iw/2)*2:ceil(ih/2)*2";
/**
* Pixel formats whose chroma subsampling requires even width AND height.
* 4:2:0 family only: yuv420p (H.264 SDR), yuv420p10le (H.265 HDR), and the
* full-range yuvj420p Chrome screenshots arrive as. ProRes 4444
* (yuva444p10le) and other 4:4:4 / alpha formats sample chroma per-pixel and
* accept odd dimensions, so they are deliberately excluded — padding them
* would needlessly distort transparent output.
*/
export function requiresEvenDimensions(pixelFormat: string): boolean {
return pixelFormat.startsWith("yuv420") || pixelFormat.startsWith("yuvj420");
}
/**
* Append the even-dimension pad to an FFmpeg `-vf` chain when the target pixel
* format requires it. When both dimensions are known and already even, omit
* the filter entirely so minimal FFmpeg builds do not need to provide `pad`.
* Returns the chain unchanged for formats that accept odd dimensions, and
* returns just the pad when there is no existing chain.
*/
export function withEvenDimensionPad(
vfChain: string,
pixelFormat: string,
width?: number,
height?: number,
): string {
if (!requiresEvenDimensions(pixelFormat)) return vfChain;
if (width !== undefined && height !== undefined && width % 2 === 0 && height % 2 === 0) {
return vfChain;
}
return vfChain ? `${vfChain},${EVEN_DIMENSION_PAD}` : EVEN_DIMENSION_PAD;
}