Files
hyperframes/packages/engine/src/utils/gpuEncoder.test.ts
T
roiizchakandroi32 3b8de7a5eb fix(engine): accept libx264 preset names with NVENC and QSV (#442)
NVENC rejects the libx264 preset vocabulary (ultrafast / medium / slow /
...) with AVERROR(EINVAL) ("Error applying encoder options: Invalid
argument"), which surfaces as a bare `FFmpeg exited with code -22` from
spawn(). Because ENCODER_PRESETS passes these names straight through to
h264_nvenc / hevc_nvenc, every `--gpu` render using the `draft` tier
failed; `standard` (medium) and `high` (slow) only worked coincidentally
on ffmpeg builds that happened to accept those aliases. QSV has the same
problem on a narrower set (ultrafast / superfast / placebo).

Add `mapPresetForGpuEncoder` in utils/gpuEncoder.ts that translates the
libx264 vocabulary to each encoder's native names:

- nvenc: libx264 -> p1..p7 (already-native pN values pass through);
  unknown values fall back to p4 (medium)
- qsv:   ultrafast / superfast -> veryfast; placebo -> veryslow;
  everything else passes through
- videotoolbox / vaapi / null: unchanged

Both buildEncoderArgs (chunkEncoder.ts) and buildStreamingArgs
(streamingEncoder.ts) now route through the helper before pushing
`-preset` to the ffmpeg arg vector.

To make the next encoder-options failure diagnosable without re-running
ffmpeg by hand, \`formatFfmpegError\` in utils/runFfmpeg.ts now appends
the last 15 non-empty stderr lines to the error string. The four call
sites that previously swallowed stderr (encodeFramesFromDir,
muxVideoWithAudio, applyFaststart, and the streaming encoder exit
handler) have been updated.

Tested end-to-end on an RTX 4080 with ffmpeg 8.1 NVENC across
\`--quality draft|standard|high\` plus \`--video-bitrate\` and \`--crf\`
overrides; the 6 renders were visually equivalent to the CPU baseline.

Co-authored-by: roi32 <75878108+roi32@users.noreply.github.com>
2026-04-23 08:38:12 -07:00

65 lines
1.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { mapPresetForGpuEncoder } from "./gpuEncoder.js";
describe("mapPresetForGpuEncoder", () => {
describe("nvenc", () => {
it.each([
["ultrafast", "p1"],
["superfast", "p1"],
["veryfast", "p2"],
["faster", "p3"],
["fast", "p4"],
["medium", "p4"],
["slow", "p5"],
["slower", "p6"],
["veryslow", "p7"],
["placebo", "p7"],
])("maps libx264 preset %s to NVENC %s", (input, expected) => {
expect(mapPresetForGpuEncoder("nvenc", input)).toBe(expected);
});
it.each(["p1", "p2", "p3", "p4", "p5", "p6", "p7"])(
"passes NVENC-native preset %s through unchanged",
(preset) => {
expect(mapPresetForGpuEncoder("nvenc", preset)).toBe(preset);
},
);
it("falls back to p4 for unknown preset values", () => {
expect(mapPresetForGpuEncoder("nvenc", "nonsense")).toBe("p4");
});
});
describe("qsv", () => {
it.each([
["ultrafast", "veryfast"],
["superfast", "veryfast"],
["placebo", "veryslow"],
])("rewrites libx264-only preset %s to QSV-supported %s", (input, expected) => {
expect(mapPresetForGpuEncoder("qsv", input)).toBe(expected);
});
it.each(["veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"])(
"passes supported preset %s through unchanged",
(preset) => {
expect(mapPresetForGpuEncoder("qsv", preset)).toBe(preset);
},
);
});
describe("other encoders", () => {
it.each(["videotoolbox", "vaapi"] as const)(
"passes preset through unchanged for %s",
(encoder) => {
expect(mapPresetForGpuEncoder(encoder, "medium")).toBe("medium");
expect(mapPresetForGpuEncoder(encoder, "ultrafast")).toBe("ultrafast");
},
);
it("passes preset through unchanged when encoder is null (CPU)", () => {
expect(mapPresetForGpuEncoder(null, "ultrafast")).toBe("ultrafast");
});
});
});