mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(engine): support AMD AMF GPU encoding
This commit is contained in:
+1
-1
@@ -164,7 +164,7 @@ The container runs the same probe → composite → encode pipeline as the local
|
||||
- **MP4 only** — HDR output with `--format mov` or `--format webm` falls back to SDR
|
||||
- **HDR images: 16-bit PNG only** — other formats (JPEG, WebP, AVIF, APNG) are not decoded as HDR and fall through the SDR DOM path
|
||||
- **H.265 only — H.264 is stripped** — calling the encoder with `codec: "h264"` and `hdr: { transfer }` is rejected; the encoder logs a warning, drops `hdr`, and tags the output as SDR/BT.709. `libx264` cannot encode HDR, so the alternative would be a "half-HDR" file (BT.2020 container tags but a BT.709 VUI block in the bitstream) which confuses HDR-aware players.
|
||||
- **GPU H.265 emits color tags but no static mastering metadata** — `useGpu: true` with HDR (nvenc, videotoolbox, qsv, vaapi) tags the stream with BT.2020 + the correct transfer (smpte2084 / arib-std-b67) but does **not** embed `master-display` or `max-cll` SEI. ffmpeg does not let those flags pass through hardware encoders. The output is suitable for previews and authoring but not for HDR10-aware delivery (Apple TV, YouTube, Netflix). For spec-compliant HDR10 production output, leave `useGpu: false` so the SW `libx265` path embeds the mastering metadata.
|
||||
- **GPU H.265 emits color tags but no static mastering metadata** — `useGpu: true` with HDR (nvenc, videotoolbox, amf, qsv, vaapi) tags the stream with BT.2020 + the correct transfer (smpte2084 / arib-std-b67) but does **not** embed `master-display` or `max-cll` SEI. ffmpeg does not let those flags pass through hardware encoders. The output is suitable for previews and authoring but not for HDR10-aware delivery (Apple TV, YouTube, Netflix). For spec-compliant HDR10 production output, leave `useGpu: false` so the SW `libx265` path embeds the mastering metadata.
|
||||
- **Player support** — the [`<hyperframes-player>`](/packages/player) web component plays back the encoded MP4 in the browser and inherits whatever HDR support the host browser provides; it does not implement its own HDR pipeline
|
||||
- **Headed Chrome HDR DOM capture** — the engine ships a separate WebGPU-based capture path for rendering CSS-animated DOM directly into HDR (`initHdrReadback`, `launchHdrBrowser`). It requires headed Chrome with `--enable-unsafe-webgpu` and is not used by the default render pipeline. See [Engine: HDR](/packages/engine#hdr-apis) if you are building a custom integration.
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4, MOV, or W
|
||||
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate encoding. Cannot combine with `--crf` |
|
||||
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
|
||||
| `--max-concurrent-renders` | 1-10 | 2 | Max simultaneous renders via the producer server (see [Concurrent Renders](#concurrent-renders) below) |
|
||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
|
||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV) |
|
||||
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
|
||||
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected (MP4 only). See [HDR Rendering](/guides/hdr) |
|
||||
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
||||
@@ -156,7 +156,7 @@ npx hyperframes render --video-bitrate 10M --output controlled.mp4
|
||||
|
||||
Hyperframes has two separate GPU acceleration surfaces:
|
||||
|
||||
- `--gpu` uses a hardware video encoder in FFmpeg when one is available. Supported backends include VideoToolbox on macOS, NVENC on NVIDIA systems, VAAPI on Linux, and Intel QSV on supported Windows/Linux hosts.
|
||||
- `--gpu` uses a hardware video encoder in FFmpeg when one is available. Supported backends include VideoToolbox on macOS, NVENC on NVIDIA systems, AMD AMF on Windows, VAAPI on Linux, and Intel QSV on supported Windows/Linux hosts.
|
||||
- Browser GPU uses the host GPU for local Chrome/WebGL capture. It is enabled automatically for local renders and disabled in Docker. Use `--no-browser-gpu` to opt out.
|
||||
|
||||
```bash Terminal
|
||||
|
||||
@@ -616,7 +616,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
|
||||
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
|
||||
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
||||
| `--workers` | 1-8 | 4 | Parallel render workers |
|
||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, VAAPI, QSV) |
|
||||
| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV) |
|
||||
| `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
|
||||
| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
|
||||
| `--quiet` | — | off | Suppress verbose output |
|
||||
|
||||
@@ -175,7 +175,7 @@ await applyFaststart(inputPath, outputPath);
|
||||
|
||||
// Detect GPU encoding support
|
||||
const gpu = await detectGpuEncoder();
|
||||
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null
|
||||
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf" | null
|
||||
```
|
||||
|
||||
#### WebM with VP9 Alpha
|
||||
|
||||
@@ -217,6 +217,7 @@ The producer supports hardware-accelerated encoding for faster renders:
|
||||
| macOS | VideoToolbox | Auto-detected |
|
||||
| Linux | VAAPI | Auto-detected |
|
||||
| Intel | QSV | Auto-detected |
|
||||
| AMD on Windows | AMF | Auto-detected |
|
||||
|
||||
When GPU encoding is enabled, Hyperframes detects the available FFmpeg hardware encoder automatically. To check your system's capabilities:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Requires: Docker installed and running.
|
||||
- `-w, --workers` — Parallel workers 1-8 (default: auto)
|
||||
- `--crf` — Override encoder CRF (mutually exclusive with `--video-bitrate`)
|
||||
- `--video-bitrate` — Target video bitrate such as `10M` (mutually exclusive with `--crf`)
|
||||
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, VAAPI, QSV)
|
||||
- `--gpu` — Use GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV)
|
||||
- `--browser-gpu` / `--no-browser-gpu` — Force host GPU or software (SwiftShader) for Chrome/WebGL capture. Default for local renders is `auto` — probe WebGL availability on first launch and fall back to software if no GPU is reachable. Docker mode always uses software.
|
||||
- `-o, --output` — Custom output path
|
||||
|
||||
|
||||
@@ -256,6 +256,28 @@ describe("buildEncoderArgs GPU preset mapping", () => {
|
||||
);
|
||||
expect(presetArg(args)).toBe("medium");
|
||||
});
|
||||
|
||||
it("uses AMD AMF encoder names and quality flags when selected", () => {
|
||||
const h264Args = buildEncoderArgs(
|
||||
{ ...baseOptions, codec: "h264", preset: "medium", quality: 23, useGpu: true },
|
||||
inputArgs,
|
||||
"out.mp4",
|
||||
"amf",
|
||||
);
|
||||
expect(h264Args[h264Args.indexOf("-c:v") + 1]).toBe("h264_amf");
|
||||
expect(h264Args[h264Args.indexOf("-qp_i") + 1]).toBe("23");
|
||||
expect(h264Args).toContain("-bf");
|
||||
expect(h264Args[h264Args.indexOf("-bf") + 1]).toBe("0");
|
||||
|
||||
const h265Args = buildEncoderArgs(
|
||||
{ ...baseOptions, codec: "h265", preset: "medium", quality: 23, useGpu: true },
|
||||
inputArgs,
|
||||
"out.mp4",
|
||||
"amf",
|
||||
);
|
||||
expect(h265Args[h265Args.indexOf("-c:v") + 1]).toBe("hevc_amf");
|
||||
expect(h265Args[h265Args.indexOf("-qp_i") + 1]).toBe("23");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildEncoderArgs color space", () => {
|
||||
@@ -529,7 +551,7 @@ describe("buildEncoderArgs lockGopForChunkConcat", () => {
|
||||
|
||||
it("true is a no-op on GPU encoders", () => {
|
||||
// GPU encoders take a separate code path; lockGopForChunkConcat does not
|
||||
// wire `-g` / `-keyint_min` into nvenc/qsv/vaapi.
|
||||
// wire `-g` / `-keyint_min` into nvenc/amf/qsv/vaapi.
|
||||
const args = buildEncoderArgs(
|
||||
{
|
||||
...baseOptions,
|
||||
@@ -816,7 +838,7 @@ describe("buildEncoderArgs HDR color space", () => {
|
||||
});
|
||||
|
||||
it("tags BT.2020 + transfer for HDR GPU H.265 (no mastering metadata via -x265-params)", () => {
|
||||
// GPU encoders (nvenc, videotoolbox, qsv, vaapi) still emit the BT.2020
|
||||
// GPU encoders (nvenc, videotoolbox, amf, qsv, vaapi) still emit the BT.2020
|
||||
// color tags via the codec-level -colorspace/-color_primaries/-color_trc
|
||||
// flags, but cannot accept x265-params, so HDR static mastering metadata
|
||||
// (master-display, max-cll) is not embedded. Acceptable for previews,
|
||||
|
||||
@@ -140,9 +140,13 @@ export function buildEncoderArgs(
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-global_quality", String(quality));
|
||||
break;
|
||||
case "amf":
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-rc", "cqp", "-qp_i", String(quality), "-qp_p", String(quality));
|
||||
break;
|
||||
}
|
||||
|
||||
// Same B-frame story as the SW branch below — nvenc emits B-frames
|
||||
// Same B-frame story as the SW branch below — nvenc/amf emit B-frames
|
||||
// by default (qsv via b_strategy, vaapi too), and the negative-DTS
|
||||
// freeze hits the same downstream players. The unconditional
|
||||
// `-avoid_negative_ts make_zero` near the bottom of this function
|
||||
@@ -153,7 +157,10 @@ export function buildEncoderArgs(
|
||||
// negative DTS in practice on macOS Sonoma+.
|
||||
if (
|
||||
codec === "h264" &&
|
||||
(gpuEncoder === "nvenc" || gpuEncoder === "qsv" || gpuEncoder === "vaapi")
|
||||
(gpuEncoder === "nvenc" ||
|
||||
gpuEncoder === "qsv" ||
|
||||
gpuEncoder === "vaapi" ||
|
||||
gpuEncoder === "amf")
|
||||
) {
|
||||
args.push("-bf", "0");
|
||||
if (gpuEncoder === "qsv") {
|
||||
|
||||
@@ -265,6 +265,26 @@ describe("buildStreamingArgs", () => {
|
||||
const args = buildStreamingArgs({ ...baseGpu, preset: "medium" }, "/tmp/out.mp4", "qsv");
|
||||
expect(presetArg(args)).toBe("medium");
|
||||
});
|
||||
|
||||
it("uses AMD AMF encoder names and quality flags when selected", () => {
|
||||
const h264Args = buildStreamingArgs(
|
||||
{ ...baseGpu, preset: "medium", quality: 23 },
|
||||
"/tmp/out.mp4",
|
||||
"amf",
|
||||
);
|
||||
expect(h264Args[h264Args.indexOf("-c:v") + 1]).toBe("h264_amf");
|
||||
expect(h264Args[h264Args.indexOf("-qp_i") + 1]).toBe("23");
|
||||
expect(h264Args).toContain("-bf");
|
||||
expect(h264Args[h264Args.indexOf("-bf") + 1]).toBe("0");
|
||||
|
||||
const h265Args = buildStreamingArgs(
|
||||
{ ...baseGpu, codec: "h265", preset: "medium", quality: 23 },
|
||||
"/tmp/out.mp4",
|
||||
"amf",
|
||||
);
|
||||
expect(h265Args[h265Args.indexOf("-c:v") + 1]).toBe("hevc_amf");
|
||||
expect(h265Args[h265Args.indexOf("-qp_i") + 1]).toBe("23");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -231,14 +231,21 @@ export function buildStreamingArgs(
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-global_quality", String(quality));
|
||||
break;
|
||||
case "amf":
|
||||
if (bitrate) args.push("-b:v", bitrate);
|
||||
else args.push("-rc", "cqp", "-qp_i", String(quality), "-qp_p", String(quality));
|
||||
break;
|
||||
}
|
||||
|
||||
// Mirror SW branch: GPU h264 paths emit B-frames by default (nvenc, qsv,
|
||||
// vaapi) and produce the same negative-DTS freeze for downstream players.
|
||||
// Mirror SW branch: GPU h264 paths emit B-frames by default (nvenc, amf,
|
||||
// qsv, vaapi) and produce the same negative-DTS freeze for downstream players.
|
||||
// See chunkEncoder.buildEncoderArgs for the full explanation.
|
||||
if (
|
||||
codec === "h264" &&
|
||||
(gpuEncoder === "nvenc" || gpuEncoder === "qsv" || gpuEncoder === "vaapi")
|
||||
(gpuEncoder === "nvenc" ||
|
||||
gpuEncoder === "qsv" ||
|
||||
gpuEncoder === "vaapi" ||
|
||||
gpuEncoder === "amf")
|
||||
) {
|
||||
args.push("-bf", "0");
|
||||
if (gpuEncoder === "qsv") {
|
||||
|
||||
@@ -1,6 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { mapPresetForGpuEncoder } from "./gpuEncoder.js";
|
||||
import {
|
||||
getCompiledGpuEncoders,
|
||||
getGpuEncoderName,
|
||||
mapPresetForGpuEncoder,
|
||||
selectUsableGpuEncoder,
|
||||
} from "./gpuEncoder.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("getCompiledGpuEncoders", () => {
|
||||
it("recognizes AMD AMF in FFmpeg's encoder list", () => {
|
||||
expect(
|
||||
getCompiledGpuEncoders(`
|
||||
V....D h264_nvenc NVIDIA NVENC H.264 encoder
|
||||
V....D h264_amf AMD AMF H.264 Encoder
|
||||
V....D h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video)
|
||||
`),
|
||||
).toEqual(["nvenc", "qsv", "amf"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectUsableGpuEncoder", () => {
|
||||
it("runs probe checks concurrently while preserving candidate priority", async () => {
|
||||
vi.useFakeTimers();
|
||||
const started: string[] = [];
|
||||
const usable = selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
|
||||
started.push(encoder);
|
||||
await new Promise((resolve) => setTimeout(resolve, encoder === "nvenc" ? 50 : 1));
|
||||
return true;
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(started).toEqual(["nvenc", "amf"]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(49);
|
||||
expect(await usable).toBe("nvenc");
|
||||
});
|
||||
|
||||
it("falls through from compiled-but-unusable NVENC to usable AMD AMF", async () => {
|
||||
const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
|
||||
return encoder === "amf";
|
||||
});
|
||||
|
||||
expect(usable).toBe("amf");
|
||||
});
|
||||
|
||||
it("treats rejected probe checks as unusable", async () => {
|
||||
const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
|
||||
if (encoder === "nvenc") {
|
||||
throw new Error("driver probe failed");
|
||||
}
|
||||
return encoder === "amf";
|
||||
});
|
||||
|
||||
expect(usable).toBe("amf");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGpuEncoderName", () => {
|
||||
it("maps AMD AMF to FFmpeg's h264 and hevc encoder names", () => {
|
||||
expect(getGpuEncoderName("amf", "h264")).toBe("h264_amf");
|
||||
expect(getGpuEncoderName("amf", "h265")).toBe("hevc_amf");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapPresetForGpuEncoder", () => {
|
||||
describe("nvenc", () => {
|
||||
@@ -49,7 +114,7 @@ describe("mapPresetForGpuEncoder", () => {
|
||||
});
|
||||
|
||||
describe("other encoders", () => {
|
||||
it.each(["videotoolbox", "vaapi"] as const)(
|
||||
it.each(["videotoolbox", "vaapi", "amf"] as const)(
|
||||
"passes preset through unchanged for %s",
|
||||
(encoder) => {
|
||||
expect(mapPresetForGpuEncoder(encoder, "medium")).toBe("medium");
|
||||
|
||||
@@ -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