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>
This commit is contained in:
roiizchak
2026-04-23 08:38:12 -07:00
committed by GitHub
co-authored by roi32
parent 8ffd007716
commit 3b8de7a5eb
8 changed files with 316 additions and 19 deletions
@@ -161,6 +161,46 @@ describe("buildStreamingArgs", () => {
expect(args[args.length - 1]).toBe("/tmp/some-output.mp4");
});
});
describe("GPU preset mapping", () => {
const baseGpu: StreamingEncoderOptions = {
fps: 30,
width: 1920,
height: 1080,
codec: "h264",
preset: "ultrafast",
quality: 28,
useGpu: true,
};
function presetArg(args: string[]): string | undefined {
const idx = args.indexOf("-preset");
return idx === -1 ? undefined : args[idx + 1];
}
// Regression for the streaming-encode + --gpu failure: NVENC rejects
// libx264 `ultrafast` with AVERROR(EINVAL), which previously surfaced
// as a bare "FFmpeg exited with code -22".
it("translates ultrafast to NVENC p1", () => {
const args = buildStreamingArgs(baseGpu, "/tmp/out.mp4", "nvenc");
expect(presetArg(args)).toBe("p1");
});
it("translates medium to NVENC p4", () => {
const args = buildStreamingArgs({ ...baseGpu, preset: "medium" }, "/tmp/out.mp4", "nvenc");
expect(presetArg(args)).toBe("p4");
});
it("rewrites QSV's unsupported ultrafast preset to veryfast", () => {
const args = buildStreamingArgs(baseGpu, "/tmp/out.mp4", "qsv");
expect(presetArg(args)).toBe("veryfast");
});
it("passes QSV-supported preset names through unchanged", () => {
const args = buildStreamingArgs({ ...baseGpu, preset: "medium" }, "/tmp/out.mp4", "qsv");
expect(presetArg(args)).toBe("medium");
});
});
});
describe("createFrameReorderBuffer", () => {