mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-02 19:57:13 +00:00
fix(engine): GPU renders use the NVENC preset names the host's ffmpeg lists (#6930)
* fix(engine): GPU renders use the NVENC preset names the host's ffmpeg lists * fix(engine): one ffmpeg stdout reader for encoder detection and preset listing * ci(engine): raise the engine unpacked size budget for the NVENC preset reader
This commit is contained in:
@@ -1061,6 +1061,18 @@ describe("buildEncoderArgs GPU preset mapping", () => {
|
||||
expect(presetArg(args)).toBe("p1");
|
||||
});
|
||||
|
||||
it("gives an NVENC build that lists only legacy presets a legacy preset, not p4", () => {
|
||||
const legacy = new Set(["default", "slow", "medium", "fast", "hp", "hq"]);
|
||||
const args = buildEncoderArgs(
|
||||
{ ...baseOptions, codec: "h264", preset: "medium", quality: 18, useGpu: true },
|
||||
inputArgs,
|
||||
"out.mp4",
|
||||
"nvenc",
|
||||
legacy,
|
||||
);
|
||||
expect(presetArg(args)).toBe("medium");
|
||||
});
|
||||
|
||||
it("translates the standard medium preset to NVENC p4", () => {
|
||||
const args = buildEncoderArgs(
|
||||
{ ...baseOptions, codec: "h264", preset: "medium", quality: 18, useGpu: true },
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
||||
import {
|
||||
type GpuEncoder,
|
||||
getCachedGpuEncoder,
|
||||
getCachedNvencPresets,
|
||||
getGpuEncoderName,
|
||||
buildVideoToolboxRateControlArgs,
|
||||
mapPresetForGpuEncoder,
|
||||
@@ -284,6 +285,7 @@ export function buildEncoderArgs(
|
||||
inputArgs: string[],
|
||||
outputPath: string,
|
||||
gpuEncoder: GpuEncoder = null,
|
||||
nvencPresets?: ReadonlySet<string>,
|
||||
): string[] {
|
||||
const {
|
||||
fps,
|
||||
@@ -320,7 +322,7 @@ export function buildEncoderArgs(
|
||||
|
||||
switch (gpuEncoder) {
|
||||
case "nvenc":
|
||||
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
|
||||
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset, nvencPresets));
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-cq", String(quality));
|
||||
break;
|
||||
@@ -620,7 +622,8 @@ export async function encodeFramesFromDir(
|
||||
|
||||
const inputPath = frameSequencePath(framesDir, framePattern);
|
||||
const inputArgs = ["-framerate", fpsToFfmpegArg(options.fps), "-i", inputPath];
|
||||
const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder);
|
||||
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
||||
const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder, nvencPresets);
|
||||
const encodeTimeout = config?.ffmpegEncodeTimeout ?? DEFAULT_CONFIG.ffmpegEncodeTimeout;
|
||||
let framesRead = 0;
|
||||
const result = await runFfmpeg(args, {
|
||||
@@ -796,7 +799,8 @@ export async function encodeFramesChunkedConcat(
|
||||
];
|
||||
let gpuEncoder: GpuEncoder = null;
|
||||
if (options.useGpu) gpuEncoder = await getCachedGpuEncoder();
|
||||
const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder);
|
||||
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
||||
const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder, nvencPresets);
|
||||
const encodeTimeout = config?.ffmpegEncodeTimeout ?? DEFAULT_CONFIG.ffmpegEncodeTimeout;
|
||||
let framesRead = 0;
|
||||
const processResult = await runFfmpeg(args, {
|
||||
|
||||
@@ -103,6 +103,18 @@ describe("buildStreamingArgs", () => {
|
||||
expectSingleHvc1Tag(args);
|
||||
});
|
||||
|
||||
it("gives an NVENC build that lists only legacy presets a legacy preset, not p4", () => {
|
||||
const legacy = new Set(["default", "slow", "medium", "fast", "hp", "hq"]);
|
||||
const args = buildStreamingArgs(
|
||||
{ ...baseSdr, codec: "h265", useGpu: true, preset: "medium" },
|
||||
"/tmp/out.mp4",
|
||||
"nvenc",
|
||||
legacy,
|
||||
);
|
||||
|
||||
expect(args[args.indexOf("-preset") + 1]).toBe("medium");
|
||||
});
|
||||
|
||||
it("keeps the software tag when a detected GPU is disabled", () => {
|
||||
const args = buildStreamingArgs({ ...baseSdr, codec: "h265" }, "/tmp/out.mp4", "nvenc");
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { dirname } from "path";
|
||||
import {
|
||||
type GpuEncoder,
|
||||
getCachedGpuEncoder,
|
||||
getCachedNvencPresets,
|
||||
getGpuEncoderName,
|
||||
buildVideoToolboxRateControlArgs,
|
||||
mapPresetForGpuEncoder,
|
||||
@@ -233,6 +234,7 @@ export function buildStreamingArgs(
|
||||
options: StreamingEncoderOptions,
|
||||
outputPath: string,
|
||||
gpuEncoder: GpuEncoder = null,
|
||||
nvencPresets?: ReadonlySet<string>,
|
||||
): string[] {
|
||||
const {
|
||||
fps,
|
||||
@@ -312,7 +314,7 @@ export function buildStreamingArgs(
|
||||
|
||||
switch (gpuEncoder) {
|
||||
case "nvenc":
|
||||
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
|
||||
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset, nvencPresets));
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-cq", String(quality));
|
||||
break;
|
||||
@@ -521,7 +523,8 @@ export async function spawnStreamingEncoder(
|
||||
gpuEncoder = await getCachedGpuEncoder();
|
||||
}
|
||||
|
||||
const args = buildStreamingArgs(options, outputPath, gpuEncoder);
|
||||
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
||||
const args = buildStreamingArgs(options, outputPath, gpuEncoder, nvencPresets);
|
||||
|
||||
const ffmpeg: ChildProcess = spawn(getFfmpegBinary(), args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getGpuEncoderName,
|
||||
getProbeArgs,
|
||||
mapPresetForGpuEncoder,
|
||||
parseEncoderPresetNames,
|
||||
resetCachedGpuEncoder,
|
||||
selectUsableGpuEncoder,
|
||||
} from "./gpuEncoder.js";
|
||||
@@ -97,6 +98,37 @@ describe("mapPresetForGpuEncoder", () => {
|
||||
it("falls back to p4 for unknown preset values", () => {
|
||||
expect(mapPresetForGpuEncoder("nvenc", "nonsense")).toBe("p4");
|
||||
});
|
||||
|
||||
describe("on an ffmpeg build whose encoder lists only the legacy presets", () => {
|
||||
const legacy = parseEncoderPresetNames(LEGACY_NVENC_HELP);
|
||||
|
||||
it.each([
|
||||
["ultrafast", "hp"],
|
||||
["superfast", "hp"],
|
||||
["veryfast", "hp"],
|
||||
["faster", "fast"],
|
||||
["fast", "fast"],
|
||||
["medium", "medium"],
|
||||
["slow", "slow"],
|
||||
["slower", "slow"],
|
||||
["veryslow", "slow"],
|
||||
["placebo", "slow"],
|
||||
["p4", "medium"],
|
||||
["nonsense", "medium"],
|
||||
])("maps %s to the legacy preset %s", (input, expected) => {
|
||||
expect(mapPresetForGpuEncoder("nvenc", input, legacy)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the pN names when the encoder lists them", () => {
|
||||
const modern = parseEncoderPresetNames(MODERN_NVENC_HELP);
|
||||
expect(mapPresetForGpuEncoder("nvenc", "slow", modern)).toBe("p5");
|
||||
expect(mapPresetForGpuEncoder("nvenc", "p7", modern)).toBe("p7");
|
||||
});
|
||||
|
||||
it("keeps the pN names when the encoder's list could not be read", () => {
|
||||
expect(mapPresetForGpuEncoder("nvenc", "slow", new Set())).toBe("p5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("qsv", () => {
|
||||
@@ -157,3 +189,51 @@ describe("getCachedGpuEncoder", () => {
|
||||
expect(getCachedGpuEncoder()).toBe(first);
|
||||
});
|
||||
});
|
||||
|
||||
const LEGACY_NVENC_HELP = `Encoder h264_nvenc [NVIDIA NVENC H.264 encoder]:
|
||||
General capabilities: dr1 delay hardware
|
||||
h264_nvenc AVOptions:
|
||||
-preset <int> E..V....... Set the encoding preset (from 0 to 11) (default medium)
|
||||
default 0 E..V.......
|
||||
slow 1 E..V....... hq 2 passes
|
||||
medium 2 E..V....... hq 1 pass
|
||||
fast 3 E..V....... hp 1 pass
|
||||
hp 4 E..V.......
|
||||
hq 5 E..V.......
|
||||
bd 6 E..V.......
|
||||
ll 7 E..V....... low latency
|
||||
llhq 8 E..V....... low latency hq
|
||||
llhp 9 E..V....... low latency hp
|
||||
lossless 10 E..V.......
|
||||
losslesshp 11 E..V.......
|
||||
-profile <int> E..V....... Set the encoding profile (from 0 to 3) (default main)
|
||||
baseline 0 E..V.......
|
||||
main 1 E..V.......
|
||||
`;
|
||||
|
||||
const MODERN_NVENC_HELP = `Encoder h264_nvenc [NVIDIA NVENC H.264 encoder]:
|
||||
h264_nvenc AVOptions:
|
||||
-preset <int> E..V....... Set the encoding preset (from 0 to 18) (default p4)
|
||||
default 0 E..V.......
|
||||
slow 1 E..V....... hq 2 passes
|
||||
p1 12 E..V....... fastest (lowest quality)
|
||||
p4 15 E..V....... medium (default)
|
||||
p7 18 E..V....... slowest (best quality)
|
||||
-tune <int> E..V....... Set the encoding tuning info (from 1 to 4) (default hq)
|
||||
hq 1 E..V.......
|
||||
`;
|
||||
|
||||
describe("parseEncoderPresetNames", () => {
|
||||
it("lists the names under -preset and stops at the next option", () => {
|
||||
expect([...parseEncoderPresetNames(LEGACY_NVENC_HELP)].sort()).toEqual(
|
||||
"bd default fast hp hq ll llhp llhq lossless losslesshp medium slow".split(" "),
|
||||
);
|
||||
const modern = parseEncoderPresetNames(MODERN_NVENC_HELP);
|
||||
expect(modern.has("p4")).toBe(true);
|
||||
expect(modern.has("main")).toBe(false);
|
||||
});
|
||||
|
||||
it("is empty for text with no -preset option", () => {
|
||||
expect(parseEncoderPresetNames("Codec 'h264_nvenc' is not recognized by FFmpeg.").size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,9 +61,14 @@ export async function selectUsableGpuEncoder(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
||||
const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
/** ffmpeg's stdout, or null when it did not exit cleanly within the deadline. */
|
||||
async function ffmpegStdout(
|
||||
args: string[],
|
||||
deadlineMs: number,
|
||||
terminationGraceMs?: number,
|
||||
): Promise<string | null> {
|
||||
const ffmpeg = spawn(getFfmpegBinary(), args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
|
||||
windowsHide: true,
|
||||
});
|
||||
@@ -73,9 +78,15 @@ export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
||||
stdout += data.toString();
|
||||
});
|
||||
const outcome = await new ManagedChildProcess(ffmpeg, {
|
||||
deadlineAtMs: Date.now() + 30_000,
|
||||
deadlineAtMs: Date.now() + deadlineMs,
|
||||
terminationGraceMs,
|
||||
}).wait();
|
||||
if (outcome.reason !== "exit" || outcome.exitCode !== 0) return null;
|
||||
return outcome.reason === "exit" && outcome.exitCode === 0 ? stdout : null;
|
||||
}
|
||||
|
||||
export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
||||
const stdout = await ffmpegStdout(["-encoders"], 30_000);
|
||||
if (stdout === null) return null;
|
||||
const candidates = getCompiledGpuEncoders(stdout);
|
||||
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => null);
|
||||
}
|
||||
@@ -265,9 +276,15 @@ const QSV_PRESET_MAP: Record<string, string> = {
|
||||
* - `videotoolbox`, `vaapi`, `amf`, `null`: no remap (they either ignore
|
||||
* `-preset` entirely or accept the libx264 vocabulary).
|
||||
*/
|
||||
export function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): string {
|
||||
export function mapPresetForGpuEncoder(
|
||||
encoder: GpuEncoder,
|
||||
preset: string,
|
||||
nvencPresets?: ReadonlySet<string>,
|
||||
): string {
|
||||
switch (encoder) {
|
||||
case "nvenc":
|
||||
if (nvencPresets && nvencPresets.size > 0 && !nvencPresets.has("p4"))
|
||||
return legacyNvencPreset(preset, nvencPresets);
|
||||
if (/^p[1-7]$/.test(preset)) return preset;
|
||||
return NVENC_PRESET_MAP[preset] ?? "p4";
|
||||
case "qsv":
|
||||
@@ -276,3 +293,55 @@ export function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): str
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
// Builds on an older NVENC SDK list `default slow medium fast hp hq ...` and reject p1..p7.
|
||||
const LEGACY_NVENC_PRESET_MAP: Record<string, string> = {
|
||||
ultrafast: "hp",
|
||||
superfast: "hp",
|
||||
veryfast: "hp",
|
||||
faster: "fast",
|
||||
fast: "fast",
|
||||
medium: "medium",
|
||||
slow: "slow",
|
||||
slower: "slow",
|
||||
veryslow: "slow",
|
||||
placebo: "slow",
|
||||
};
|
||||
|
||||
function legacyNvencPreset(preset: string, accepted: ReadonlySet<string>): string {
|
||||
if (accepted.has(preset) && !/^p[1-7]$/.test(preset)) return preset;
|
||||
const mapped = LEGACY_NVENC_PRESET_MAP[preset] ?? "medium";
|
||||
return accepted.has(mapped) ? mapped : "default";
|
||||
}
|
||||
|
||||
/** The names an encoder lists under its `-preset` option in `ffmpeg -h encoder=<name>`. */
|
||||
export function parseEncoderPresetNames(helpText: string): ReadonlySet<string> {
|
||||
const names = new Set<string>();
|
||||
let inPreset = false;
|
||||
for (const line of helpText.split(/\r?\n/)) {
|
||||
if (/^\s{1,3}-\S/.test(line)) {
|
||||
inPreset = /^\s{1,3}-preset\s/.test(line);
|
||||
continue;
|
||||
}
|
||||
const entry = inPreset ? /^\s{4,}([a-z][a-z0-9_]*)\s+-?\d+\s+[E.]\S{3}/.exec(line) : null;
|
||||
if (entry) names.add(entry[1]!);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
let cachedNvencPresets: Promise<ReadonlySet<string>> | undefined = undefined;
|
||||
|
||||
/** The presets this ffmpeg's h264_nvenc takes; empty when it cannot be read, which keeps the pN names. */
|
||||
export function getCachedNvencPresets(): Promise<ReadonlySet<string>> {
|
||||
cachedNvencPresets ??= readNvencPresets();
|
||||
return cachedNvencPresets;
|
||||
}
|
||||
|
||||
async function readNvencPresets(): Promise<ReadonlySet<string>> {
|
||||
const help = await ffmpegStdout(
|
||||
["-hide_banner", "-h", "encoder=h264_nvenc"],
|
||||
GPU_PROBE_TIMEOUT_MS,
|
||||
GPU_PROBE_KILL_GRACE_MS,
|
||||
).catch(() => null);
|
||||
return help === null ? new Set() : parseEncoderPresetNames(help);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"@hyperframes/engine": {
|
||||
"packed": 456397,
|
||||
"unpacked": 1526873
|
||||
"unpacked": 1620913
|
||||
},
|
||||
"@hyperframes/gcp-cloud-run": {
|
||||
"packed": 49412,
|
||||
|
||||
Reference in New Issue
Block a user