From 0dfc85b680aaff20d89dcab4f9d937b8139eb9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 14 Jul 2026 00:42:16 -0400 Subject: [PATCH] fix(engine): skip unnecessary dimension pad (#2398) --- .../engine/src/services/chunkEncoder.test.ts | 32 +++++++++++++++++-- packages/engine/src/services/chunkEncoder.ts | 12 +++++-- .../src/services/streamingEncoder.test.ts | 15 ++++++++- .../engine/src/services/streamingEncoder.ts | 12 +++++-- .../engine/src/utils/evenDimensions.test.ts | 12 +++++++ packages/engine/src/utils/evenDimensions.ts | 16 ++++++++-- 6 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/engine/src/services/chunkEncoder.test.ts b/packages/engine/src/services/chunkEncoder.test.ts index 4014afd38..9751bc8e8 100644 --- a/packages/engine/src/services/chunkEncoder.test.ts +++ b/packages/engine/src/services/chunkEncoder.test.ts @@ -894,7 +894,17 @@ describe("buildEncoderArgs color space", () => { ); const vfIdx = args.indexOf("-vf"); expect(vfIdx).toBeGreaterThan(-1); - expect(args[vfIdx + 1]).toContain("scale=in_range=pc:out_range=tv"); + expect(args[vfIdx + 1]).toBe("scale=in_range=pc:out_range=tv"); + }); + + it("adds the pad after range conversion for odd CPU output dimensions", () => { + const args = buildEncoderArgs( + { ...baseOptions, height: 1081, codec: "h264", preset: "medium", quality: 23 }, + inputArgs, + "out.mp4", + ); + const vfIdx = args.indexOf("-vf"); + expect(args[vfIdx + 1]).toBe("scale=in_range=pc:out_range=tv,pad=ceil(iw/2)*2:ceil(ih/2)*2"); }); it("prepends range conversion to VAAPI filter chain", () => { @@ -912,7 +922,14 @@ describe("buildEncoderArgs color space", () => { it("pads odd dimensions (no range scale) for non-VAAPI GPU encoding", () => { for (const gpu of ["nvenc", "videotoolbox", "qsv", "amf"] as const) { const args = buildEncoderArgs( - { ...baseOptions, codec: "h264", preset: "medium", quality: 23, useGpu: true }, + { + ...baseOptions, + height: 1081, + codec: "h264", + preset: "medium", + quality: 23, + useGpu: true, + }, inputArgs, "out.mp4", gpu, @@ -927,10 +944,21 @@ describe("buildEncoderArgs color space", () => { } }); + it("does not require the pad filter for even GPU output dimensions", () => { + const args = buildEncoderArgs( + { ...baseOptions, codec: "h264", preset: "medium", quality: 23, useGpu: true }, + inputArgs, + "out.mp4", + "videotoolbox", + ); + expect(args).not.toContain("-vf"); + }); + it("pads odd dimensions for 10-bit (yuv420p10le) GPU HDR encoding", () => { const args = buildEncoderArgs( { ...baseOptions, + height: 1081, codec: "h265", preset: "medium", quality: 23, diff --git a/packages/engine/src/services/chunkEncoder.ts b/packages/engine/src/services/chunkEncoder.ts index 44f3e68ab..e7d9908e2 100644 --- a/packages/engine/src/services/chunkEncoder.ts +++ b/packages/engine/src/services/chunkEncoder.ts @@ -417,14 +417,22 @@ export function buildEncoderArgs( // encoder with no `-vf`. They hit the same "height not divisible by 2" // abort as libx264 on an odd-sized 4:2:0 canvas, so pad odd dimensions // up to even on the software side before the encode. - const vf = withEvenDimensionPad("", pixelFormat); + const vf = withEvenDimensionPad("", pixelFormat, options.width, options.height); if (vf) args.push("-vf", vf); } else { // Range conversion: Chrome screenshots are full-range RGB. // The scale filter handles both 8-bit and 10-bit correctly. Pad odd // dimensions up to even so libx264/libx265 (4:2:0) don't abort with // "height not divisible by 2" on an odd-sized composition canvas. - args.push("-vf", withEvenDimensionPad("scale=in_range=pc:out_range=tv", pixelFormat)); + args.push( + "-vf", + withEvenDimensionPad( + "scale=in_range=pc:out_range=tv", + pixelFormat, + options.width, + options.height, + ), + ); } // Fixed timescale for consistent A/V timing across platforms. diff --git a/packages/engine/src/services/streamingEncoder.test.ts b/packages/engine/src/services/streamingEncoder.test.ts index cd99c7554..ff0aa1076 100644 --- a/packages/engine/src/services/streamingEncoder.test.ts +++ b/packages/engine/src/services/streamingEncoder.test.ts @@ -164,6 +164,14 @@ describe("buildStreamingArgs", () => { expect(args[args.indexOf("-color_primaries:v") + 1]).toBe("bt709"); expect(args[args.indexOf("-colorspace:v") + 1]).toBe("bt709"); expect(args[args.indexOf("-color_range") + 1]).toBe("tv"); + expect(args[args.indexOf("-vf") + 1]).toBe("scale=in_range=pc:out_range=tv"); + }); + + it("adds the pad after range conversion for odd SDR output dimensions", () => { + const args = buildStreamingArgs({ ...baseSdr, height: 1081 }, "/tmp/out.mp4"); + expect(args[args.indexOf("-vf") + 1]).toBe( + "scale=in_range=pc:out_range=tv,pad=ceil(iw/2)*2:ceil(ih/2)*2", + ); }); }); @@ -315,13 +323,18 @@ describe("buildStreamingArgs", () => { // even-dim pad (and only the pad, not the SW range scale) must be added. it("pads odd dimensions (no range scale) for non-VAAPI GPU encoding", () => { for (const gpu of ["nvenc", "videotoolbox", "qsv", "amf"] as const) { - const args = buildStreamingArgs(baseGpu, "/tmp/out.mp4", gpu); + const args = buildStreamingArgs({ ...baseGpu, height: 1081 }, "/tmp/out.mp4", gpu); const vfIdx = args.indexOf("-vf"); expect(args[vfIdx + 1]).toBe("pad=ceil(iw/2)*2:ceil(ih/2)*2"); expect(args[vfIdx + 1]).not.toContain("scale=in_range"); } }); + it("does not require the pad filter for even GPU output dimensions", () => { + const args = buildStreamingArgs(baseGpu, "/tmp/out.mp4", "videotoolbox"); + expect(args).not.toContain("-vf"); + }); + it("prepends range conversion to VAAPI chain (nv12 covers even-dim)", () => { const args = buildStreamingArgs(baseGpu, "/tmp/out.mp4", "vaapi"); const vfIdx = args.indexOf("-vf"); diff --git a/packages/engine/src/services/streamingEncoder.ts b/packages/engine/src/services/streamingEncoder.ts index 109cf4041..400cc8f99 100644 --- a/packages/engine/src/services/streamingEncoder.ts +++ b/packages/engine/src/services/streamingEncoder.ts @@ -395,13 +395,21 @@ export function buildStreamingArgs( // encoder with no `-vf`. They hit the same "height not divisible by 2" // abort as libx264 on an odd-sized 4:2:0 canvas, so pad odd dimensions // up to even on the software side before the encode. - const vf = withEvenDimensionPad("", pixelFormat); + const vf = withEvenDimensionPad("", pixelFormat, options.width, options.height); if (vf) args.push("-vf", vf); } else { // Range conversion: Chrome screenshots are full-range RGB. Pad odd // dimensions up to even so libx264/libx265 (4:2:0) don't abort with // "height not divisible by 2" on an odd-sized composition canvas. - args.push("-vf", withEvenDimensionPad("scale=in_range=pc:out_range=tv", pixelFormat)); + args.push( + "-vf", + withEvenDimensionPad( + "scale=in_range=pc:out_range=tv", + pixelFormat, + options.width, + options.height, + ), + ); } // Fixed timescale for consistent A/V timing across platforms. diff --git a/packages/engine/src/utils/evenDimensions.test.ts b/packages/engine/src/utils/evenDimensions.test.ts index e43fabcd7..1a80dce85 100644 --- a/packages/engine/src/utils/evenDimensions.test.ts +++ b/packages/engine/src/utils/evenDimensions.test.ts @@ -25,6 +25,18 @@ describe("withEvenDimensionPad", () => { expect(withEvenDimensionPad("", "yuv420p")).toBe("pad=ceil(iw/2)*2:ceil(ih/2)*2"); }); + it("omits the pad when known dimensions are already even", () => { + expect(withEvenDimensionPad("", "yuv420p", 1920, 1080)).toBe(""); + expect(withEvenDimensionPad("scale=in_range=pc:out_range=tv", "yuv420p", 1920, 1080)).toBe( + "scale=in_range=pc:out_range=tv", + ); + }); + + it("keeps the pad when either known dimension is odd", () => { + expect(withEvenDimensionPad("", "yuv420p", 1921, 1080)).toBe("pad=ceil(iw/2)*2:ceil(ih/2)*2"); + expect(withEvenDimensionPad("", "yuv420p", 1920, 1081)).toBe("pad=ceil(iw/2)*2:ceil(ih/2)*2"); + }); + it("leaves the filter chain unchanged for alpha output (even in, unchanged)", () => { const vf = "scale=in_range=pc:out_range=tv"; expect(withEvenDimensionPad(vf, "yuva444p10le")).toBe(vf); diff --git a/packages/engine/src/utils/evenDimensions.ts b/packages/engine/src/utils/evenDimensions.ts index 0df602bfb..964abbf62 100644 --- a/packages/engine/src/utils/evenDimensions.ts +++ b/packages/engine/src/utils/evenDimensions.ts @@ -36,10 +36,20 @@ export function requiresEvenDimensions(pixelFormat: string): boolean { /** * Append the even-dimension pad to an FFmpeg `-vf` chain when the target pixel - * format requires it. Returns the chain unchanged for formats that accept odd - * dimensions, and returns just the pad when there is no existing chain. + * 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): string { +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; }