mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
fix(engine): support AMD AMF GPU encoding
This commit is contained in:
@@ -7,7 +7,55 @@
|
||||
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export type GpuEncoder = "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null;
|
||||
export type ConcreteGpuEncoder = "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf";
|
||||
export type GpuEncoder = ConcreteGpuEncoder | null;
|
||||
|
||||
const GPU_ENCODER_CANDIDATES: ConcreteGpuEncoder[] = [
|
||||
"nvenc",
|
||||
"videotoolbox",
|
||||
"vaapi",
|
||||
"qsv",
|
||||
"amf",
|
||||
];
|
||||
|
||||
const H264_ENCODER_BY_GPU: Record<ConcreteGpuEncoder, string> = {
|
||||
nvenc: "h264_nvenc",
|
||||
videotoolbox: "h264_videotoolbox",
|
||||
vaapi: "h264_vaapi",
|
||||
qsv: "h264_qsv",
|
||||
amf: "h264_amf",
|
||||
};
|
||||
|
||||
const GPU_PROBE_TIMEOUT_MS = 2000;
|
||||
const GPU_PROBE_KILL_GRACE_MS = 1000;
|
||||
|
||||
export function getCompiledGpuEncoders(ffmpegEncodersStdout: string): ConcreteGpuEncoder[] {
|
||||
return GPU_ENCODER_CANDIDATES.filter((encoder) =>
|
||||
ffmpegEncodersStdout.includes(H264_ENCODER_BY_GPU[encoder]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function selectUsableGpuEncoder(
|
||||
candidates: readonly ConcreteGpuEncoder[],
|
||||
isUsable: (encoder: ConcreteGpuEncoder) => Promise<boolean>,
|
||||
): Promise<GpuEncoder> {
|
||||
const results = await Promise.all(
|
||||
candidates.map(async (encoder) => {
|
||||
try {
|
||||
return { encoder, usable: await isUsable(encoder) };
|
||||
} catch {
|
||||
return { encoder, usable: false };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.usable) {
|
||||
return result.encoder;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -21,11 +69,10 @@ export async function detectGpuEncoder(): Promise<GpuEncoder> {
|
||||
});
|
||||
|
||||
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);
|
||||
const candidates = getCompiledGpuEncoders(stdout);
|
||||
void selectUsableGpuEncoder(candidates, canUseGpuEncoder)
|
||||
.then(resolve)
|
||||
.catch(() => resolve(null));
|
||||
});
|
||||
|
||||
ffmpeg.on("error", () => resolve(null));
|
||||
@@ -52,11 +99,111 @@ export function getGpuEncoderName(encoder: GpuEncoder, codec: "h264" | "h265"):
|
||||
return codec === "h264" ? "h264_vaapi" : "hevc_vaapi";
|
||||
case "qsv":
|
||||
return codec === "h264" ? "h264_qsv" : "hevc_qsv";
|
||||
case "amf":
|
||||
return codec === "h264" ? "h264_amf" : "hevc_amf";
|
||||
default:
|
||||
return codec === "h264" ? "libx264" : "libx265";
|
||||
}
|
||||
}
|
||||
|
||||
function getProbeArgs(encoder: ConcreteGpuEncoder): string[] {
|
||||
const args = [
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=size=16x16:rate=1:duration=1",
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-an",
|
||||
];
|
||||
|
||||
if (encoder === "vaapi") {
|
||||
args.push("-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload");
|
||||
}
|
||||
|
||||
args.push("-c:v", getGpuEncoderName(encoder, "h264"));
|
||||
|
||||
if (encoder === "amf") {
|
||||
args.push("-rc", "cqp", "-qp_i", "28", "-qp_p", "28");
|
||||
}
|
||||
|
||||
args.push("-f", "null", "-");
|
||||
return args;
|
||||
}
|
||||
|
||||
async function canUseGpuEncoder(encoder: ConcreteGpuEncoder): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let stderr = "";
|
||||
const finish = (usable: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (killTimer) clearTimeout(killTimer);
|
||||
resolve(usable);
|
||||
};
|
||||
const ffmpeg = spawn("ffmpeg", getProbeArgs(encoder), {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
ffmpeg.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
ffmpeg.kill("SIGTERM");
|
||||
killTimer = setTimeout(() => {
|
||||
ffmpeg.kill("SIGKILL");
|
||||
finish(false);
|
||||
}, GPU_PROBE_KILL_GRACE_MS);
|
||||
}, GPU_PROBE_TIMEOUT_MS);
|
||||
|
||||
ffmpeg.on("close", (code, signal) => {
|
||||
const usable = code === 0;
|
||||
logGpuProbeFailure(encoder, { code, signal, stderr, timedOut });
|
||||
finish(usable);
|
||||
});
|
||||
|
||||
ffmpeg.on("error", (error) => {
|
||||
logGpuProbeFailure(encoder, { error, timedOut });
|
||||
finish(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logGpuProbeFailure(
|
||||
encoder: ConcreteGpuEncoder,
|
||||
result: {
|
||||
code?: number | null;
|
||||
error?: Error;
|
||||
signal?: NodeJS.Signals | null;
|
||||
stderr?: string;
|
||||
timedOut?: boolean;
|
||||
},
|
||||
): void {
|
||||
if (!isGpuProbeDebugEnabled()) return;
|
||||
if (result.code === 0 && !result.error && !result.timedOut) return;
|
||||
|
||||
const reason = result.error
|
||||
? result.error.message
|
||||
: result.timedOut
|
||||
? `timed out after ${GPU_PROBE_TIMEOUT_MS}ms`
|
||||
: `exit=${String(result.code)} signal=${String(result.signal ?? "")}`;
|
||||
const stderr = result.stderr?.trim();
|
||||
console.warn(`[gpuEncoder] ${encoder} probe failed: ${reason}${stderr ? `\n${stderr}` : ""}`);
|
||||
}
|
||||
|
||||
function isGpuProbeDebugEnabled(): boolean {
|
||||
const value = process.env.HYPERFRAMES_DEBUG_GPU_PROBE;
|
||||
return value === "1" || value === "true";
|
||||
}
|
||||
|
||||
// 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"),
|
||||
@@ -93,8 +240,8 @@ const QSV_PRESET_MAP: Record<string, string> = {
|
||||
* 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).
|
||||
* - `videotoolbox`, `vaapi`, `amf`, `null`: no remap (they either ignore
|
||||
* `-preset` entirely or accept the libx264 vocabulary).
|
||||
*/
|
||||
export function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): string {
|
||||
switch (encoder) {
|
||||
|
||||
Reference in New Issue
Block a user