forked from heygen-com/hyperframes
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgpuEncoder.ts
More file actions
242 lines (217 loc) · 7.46 KB
/
Copy pathgpuEncoder.ts
File metadata and controls
242 lines (217 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// fallow-ignore-file complexity
/**
* GPU Encoder Detection
*
* Shared GPU encoder detection and naming utilities used by both
* chunkEncoder and streamingEncoder services.
*/
import { spawn } from "child_process";
import { getFfmpegBinary } from "./ffmpegBinaries.js";
import { ManagedChildProcess } from "./managedChildProcess.js";
import { trackChildProcess } from "./processTracker.js";
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> {
const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
stdio: ["pipe", "pipe", "pipe"],
});
trackChildProcess(ffmpeg);
let stdout = "";
ffmpeg.stdout.on("data", (data) => {
stdout += data.toString();
});
const outcome = await new ManagedChildProcess(ffmpeg, {
deadlineAtMs: Date.now() + 30_000,
}).wait();
if (outcome.reason !== "exit" || outcome.exitCode !== 0) return null;
const candidates = getCompiledGpuEncoders(stdout);
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => 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";
case "amf":
return codec === "h264" ? "h264_amf" : "hevc_amf";
default:
return codec === "h264" ? "libx264" : "libx265";
}
}
// Minimum probe dimensions must clear every GPU encoder's hardware minimum.
// NVIDIA data-center SKUs (L4/T4/A10/A100) reject frames below ~257px on
// either dimension with "Frame Dimension less than the minimum supported
// value" (observed on driver 595.58.03, CUDA 13.2). The documented SDK
// minimums (145×49 H.264, 129×33 HEVC) are lower, but the driver enforces
// a stricter per-SKU alignment. 320×240 clears all known GPU encoder
// minimums (NVENC, VideoToolbox, VAAPI, QSV, AMF) while staying cheap.
const GPU_PROBE_WIDTH = 320;
const GPU_PROBE_HEIGHT = 240;
export function getProbeArgs(encoder: ConcreteGpuEncoder): string[] {
const args = [
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
`color=size=${GPU_PROBE_WIDTH}x${GPU_PROBE_HEIGHT}: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> {
const ffmpeg = spawn(getFfmpegBinary(), getProbeArgs(encoder), {
stdio: ["ignore", "ignore", "pipe"],
});
trackChildProcess(ffmpeg);
const outcome = await new ManagedChildProcess(ffmpeg, {
deadlineAtMs: Date.now() + GPU_PROBE_TIMEOUT_MS,
terminationGraceMs: GPU_PROBE_KILL_GRACE_MS,
}).wait();
const usable = outcome.reason === "exit" && outcome.exitCode === 0;
logGpuProbeFailure(encoder, {
code: outcome.exitCode,
signal: outcome.signal,
stderr: outcome.stderr,
error: outcome.error,
timedOut: outcome.reason === "deadline",
});
return usable;
}
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"),
// 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`, `amf`, `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;
}
}