mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-08 07:46:59 +00:00
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>
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
/**
|
|
* GPU Encoder Detection
|
|
*
|
|
* Shared GPU encoder detection and naming utilities used by both
|
|
* chunkEncoder and streamingEncoder services.
|
|
*/
|
|
|
|
import { spawn } from "child_process";
|
|
|
|
export type GpuEncoder = "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null;
|
|
|
|
export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
|
return new Promise((resolve) => {
|
|
const ffmpeg = spawn("ffmpeg", ["-encoders"], {
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
|
|
ffmpeg.stdout.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
ffmpeg.on("close", () => {
|
|
if (stdout.includes("h264_nvenc")) resolve("nvenc");
|
|
else if (stdout.includes("h264_videotoolbox")) resolve("videotoolbox");
|
|
else if (stdout.includes("h264_vaapi")) resolve("vaapi");
|
|
else if (stdout.includes("h264_qsv")) resolve("qsv");
|
|
else resolve(null);
|
|
});
|
|
|
|
ffmpeg.on("error", () => resolve(null));
|
|
});
|
|
}
|
|
|
|
let cachedGpuEncoder: GpuEncoder | undefined = undefined;
|
|
|
|
export async function getCachedGpuEncoder(): Promise<GpuEncoder> {
|
|
if (cachedGpuEncoder === undefined) {
|
|
cachedGpuEncoder = await detectGpuEncoder();
|
|
}
|
|
return cachedGpuEncoder;
|
|
}
|
|
|
|
export function getGpuEncoderName(encoder: GpuEncoder, codec: "h264" | "h265"): string {
|
|
if (!encoder) return codec === "h264" ? "libx264" : "libx265";
|
|
switch (encoder) {
|
|
case "nvenc":
|
|
return codec === "h264" ? "h264_nvenc" : "hevc_nvenc";
|
|
case "videotoolbox":
|
|
return codec === "h264" ? "h264_videotoolbox" : "hevc_videotoolbox";
|
|
case "vaapi":
|
|
return codec === "h264" ? "h264_vaapi" : "hevc_vaapi";
|
|
case "qsv":
|
|
return codec === "h264" ? "h264_qsv" : "hevc_qsv";
|
|
default:
|
|
return codec === "h264" ? "libx264" : "libx265";
|
|
}
|
|
}
|
|
|
|
// libx264 preset names (ultrafast/superfast/.../placebo) mapped to the
|
|
// equivalent NVENC p1..p7 preset. NVENC rejects libx264 names with
|
|
// AVERROR(EINVAL) ("Error applying encoder options: Invalid argument"),
|
|
// which surfaces as a generic "FFmpeg exited with code -22" — so callers
|
|
// that share a single `preset` field across CPU and GPU paths (e.g. the
|
|
// `draft`/`standard`/`high` quality tiers) must translate before passing
|
|
// the value to h264_nvenc / hevc_nvenc.
|
|
const NVENC_PRESET_MAP: Record<string, string> = {
|
|
ultrafast: "p1",
|
|
superfast: "p1",
|
|
veryfast: "p2",
|
|
faster: "p3",
|
|
fast: "p4",
|
|
medium: "p4",
|
|
slow: "p5",
|
|
slower: "p6",
|
|
veryslow: "p7",
|
|
placebo: "p7",
|
|
};
|
|
|
|
// QSV accepts most libx264 preset names but rejects `ultrafast`,
|
|
// `superfast`, and `placebo`. Map those to the nearest supported values.
|
|
const QSV_PRESET_MAP: Record<string, string> = {
|
|
ultrafast: "veryfast",
|
|
superfast: "veryfast",
|
|
placebo: "veryslow",
|
|
};
|
|
|
|
/**
|
|
* Translate a libx264-style `-preset` value to one accepted by the given
|
|
* GPU encoder.
|
|
*
|
|
* - `nvenc`: libx264 names → `p1`..`p7`. Already-native `pN` values pass
|
|
* through unchanged. Unknown values fall back to `p4` (medium).
|
|
* - `qsv`: `ultrafast`/`superfast`/`placebo` → nearest supported name;
|
|
* everything else passes through.
|
|
* - `videotoolbox`, `vaapi`, `null`: no remap (they either ignore `-preset`
|
|
* entirely or accept the libx264 vocabulary).
|
|
*/
|
|
export function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): string {
|
|
switch (encoder) {
|
|
case "nvenc":
|
|
if (/^p[1-7]$/.test(preset)) return preset;
|
|
return NVENC_PRESET_MAP[preset] ?? "p4";
|
|
case "qsv":
|
|
return QSV_PRESET_MAP[preset] ?? preset;
|
|
default:
|
|
return preset;
|
|
}
|
|
}
|