Files
hyperframes/packages/engine/src/utils/runFfmpeg.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

47 lines
2.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { formatFfmpegError } from "./runFfmpeg.js";
describe("formatFfmpegError", () => {
it("reports exit code alone when stderr is empty", () => {
expect(formatFfmpegError(-22, "")).toBe("FFmpeg exited with code -22");
});
it("appends stderr tail when present", () => {
const stderr =
"ffmpeg version 8.1\nbuilt with gcc 13.2.0\n" +
"[h264_nvenc @ 0x7f] Error applying encoder options: Invalid argument\n" +
"Error while opening encoder\n";
const message = formatFfmpegError(-22, stderr);
expect(message).toContain("FFmpeg exited with code -22");
expect(message).toContain("ffmpeg stderr (tail):");
expect(message).toContain("Error applying encoder options: Invalid argument");
expect(message).toContain("Error while opening encoder");
});
it("keeps only the last N non-empty lines in the tail", () => {
const lines = Array.from({ length: 30 }, (_, i) => `line-${i}`).join("\n");
const message = formatFfmpegError(1, lines, 5);
expect(message).toContain("line-29");
expect(message).toContain("line-25");
expect(message).not.toContain("line-24");
});
it("strips blank lines from the tail so real signal isn't hidden", () => {
const stderr = "\n\nError applying encoder options: Invalid argument\n\n\n";
const message = formatFfmpegError(-22, stderr);
expect(message).toContain("Error applying encoder options: Invalid argument");
// Only one non-empty stderr line should appear in the tail.
const tailPart = message.split("ffmpeg stderr (tail):\n")[1] ?? "";
expect(tailPart.trim().split(/\r?\n/).length).toBe(1);
});
it("falls back to a process-error string when exit code is null and stderr is empty", () => {
expect(formatFfmpegError(null, "")).toBe("[FFmpeg] process error");
});
it("wraps stderr in [FFmpeg] prefix when exit code is null (spawn failure)", () => {
expect(formatFfmpegError(null, "spawn ffmpeg ENOENT")).toBe("[FFmpeg] spawn ffmpeg ENOENT");
});
});