@hyperframes/engine 0.6.119 → 0.6.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,457 +0,0 @@
1
- // fallow-ignore-file code-duplication complexity
2
- import { spawn } from "child_process";
3
- import { readFileSync } from "fs";
4
- import { extname } from "path";
5
- import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js";
6
-
7
- /** Spawn ffprobe with given args, return stdout. Throws on non-zero exit or missing binary. */
8
- function runFfprobe(args: string[]): Promise<string> {
9
- return new Promise((resolve, reject) => {
10
- const command = getFfprobeBinary();
11
- const proc = spawn(command, args);
12
- let stdout = "";
13
- let stderr = "";
14
- proc.stdout.on("data", (data) => {
15
- stdout += data.toString();
16
- });
17
- proc.stderr.on("data", (data) => {
18
- stderr += data.toString();
19
- });
20
- proc.on("close", (code) => {
21
- if (code !== 0) {
22
- reject(new Error(`[FFmpeg] ffprobe exited with code ${code}: ${stderr}`));
23
- } else {
24
- resolve(stdout);
25
- }
26
- });
27
- proc.on("error", (err) => {
28
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
29
- const configured = process.env[FFPROBE_PATH_ENV]?.trim();
30
- reject(
31
- new Error(
32
- configured
33
- ? `[FFmpeg] ffprobe not found at ${FFPROBE_PATH_ENV}="${configured}". Please install FFmpeg.`
34
- : "[FFmpeg] ffprobe not found. Please install FFmpeg.",
35
- ),
36
- );
37
- } else {
38
- reject(err);
39
- }
40
- });
41
- });
42
- }
43
-
44
- function parseProbeJson(stdout: string): FFProbeOutput {
45
- try {
46
- return JSON.parse(stdout);
47
- } catch (e) {
48
- throw new Error(
49
- `[FFmpeg] Failed to parse ffprobe output: ${e instanceof Error ? e.message : e}`,
50
- );
51
- }
52
- }
53
-
54
- const videoMetadataCache = new Map<string, Promise<VideoMetadata>>();
55
- const audioMetadataCache = new Map<string, Promise<AudioMetadata>>();
56
-
57
- export interface VideoColorSpace {
58
- /** Color transfer characteristics, e.g. "bt709", "smpte2084", "arib-std-b67" */
59
- colorTransfer: string;
60
- /** Color primaries, e.g. "bt709", "bt2020" */
61
- colorPrimaries: string;
62
- /** Color matrix/space, e.g. "bt709", "bt2020nc" */
63
- colorSpace: string;
64
- }
65
-
66
- export interface VideoMetadata {
67
- durationSeconds: number;
68
- videoStreamDurationSeconds: number;
69
- width: number;
70
- height: number;
71
- fps: number;
72
- videoCodec: string;
73
- hasAudio: boolean;
74
- /** True when r_frame_rate and avg_frame_rate differ significantly (>10%), indicating variable frame rate. */
75
- isVFR: boolean;
76
- /** True when the stream carries an alpha channel. */
77
- hasAlpha: boolean;
78
- /** Color space info from the video stream. Null if ffprobe didn't report it. */
79
- colorSpace: VideoColorSpace | null;
80
- }
81
-
82
- export interface AudioMetadata {
83
- durationSeconds: number;
84
- sampleRate: number;
85
- channels: number;
86
- audioCodec: string;
87
- bitrate?: number;
88
- }
89
-
90
- interface FFProbeStream {
91
- codec_type: string;
92
- codec_name?: string;
93
- width?: number;
94
- height?: number;
95
- duration?: string;
96
- nb_frames?: string;
97
- pix_fmt?: string;
98
- r_frame_rate?: string;
99
- avg_frame_rate?: string;
100
- sample_rate?: string;
101
- channels?: number;
102
- color_transfer?: string;
103
- color_primaries?: string;
104
- color_space?: string;
105
- tags?: Record<string, string>;
106
- }
107
-
108
- interface FFProbeFormat {
109
- duration?: string;
110
- bit_rate?: string;
111
- }
112
-
113
- interface FFProbeOutput {
114
- streams: FFProbeStream[];
115
- format: FFProbeFormat;
116
- }
117
-
118
- interface StillImageMetadata {
119
- width: number;
120
- height: number;
121
- colorSpace: VideoColorSpace | null;
122
- }
123
-
124
- function crc32(buf: Buffer): number {
125
- let crc = 0xffffffff;
126
- for (let i = 0; i < buf.length; i++) {
127
- crc ^= buf[i] ?? 0;
128
- for (let bit = 0; bit < 8; bit++) {
129
- const mask = -(crc & 1);
130
- crc = (crc >>> 1) ^ (0xedb88320 & mask);
131
- }
132
- }
133
- return (crc ^ 0xffffffff) >>> 0;
134
- }
135
-
136
- export function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | null {
137
- if (
138
- buf.length < 8 ||
139
- buf[0] !== 137 ||
140
- buf[1] !== 80 ||
141
- buf[2] !== 78 ||
142
- buf[3] !== 71 ||
143
- buf[4] !== 13 ||
144
- buf[5] !== 10 ||
145
- buf[6] !== 26 ||
146
- buf[7] !== 10
147
- ) {
148
- return null;
149
- }
150
-
151
- let width = 0;
152
- let height = 0;
153
- let seenIdat = false;
154
- let pos = 8;
155
- while (pos + 12 <= buf.length) {
156
- const chunkLen = buf.readUInt32BE(pos);
157
- const chunkType = buf.toString("ascii", pos + 4, pos + 8);
158
- if (pos + 12 + chunkLen > buf.length) return null;
159
- const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
160
- const chunkCrc = buf.readUInt32BE(pos + 8 + chunkLen);
161
- const chunkBytes = Buffer.concat([Buffer.from(chunkType, "ascii"), chunkData]);
162
- if (crc32(chunkBytes) !== chunkCrc) return null;
163
-
164
- if (chunkType === "IHDR" && chunkLen >= 8) {
165
- width = buf.readUInt32BE(pos + 8);
166
- height = buf.readUInt32BE(pos + 12);
167
- }
168
-
169
- if (chunkType === "IDAT") {
170
- seenIdat = true;
171
- }
172
-
173
- if (chunkType === "cICP" && chunkLen === 4 && !seenIdat) {
174
- const primariesCode = chunkData[0] ?? 0;
175
- const transferCode = chunkData[1] ?? 0;
176
- const matrixCode = chunkData[2] ?? 0;
177
-
178
- return {
179
- width,
180
- height,
181
- colorSpace: {
182
- colorPrimaries:
183
- primariesCode === 9
184
- ? "bt2020"
185
- : primariesCode === 1
186
- ? "bt709"
187
- : `unknown-${primariesCode}`,
188
- colorTransfer:
189
- transferCode === 16
190
- ? "smpte2084"
191
- : transferCode === 18
192
- ? "arib-std-b67"
193
- : transferCode === 1
194
- ? "bt709"
195
- : `unknown-${transferCode}`,
196
- colorSpace:
197
- matrixCode === 9 ? "bt2020nc" : matrixCode === 0 ? "gbr" : `unknown-${matrixCode}`,
198
- },
199
- };
200
- }
201
-
202
- if (chunkType === "IEND") break;
203
- pos += 12 + chunkLen;
204
- }
205
-
206
- return width > 0 && height > 0 ? { width, height, colorSpace: null } : null;
207
- }
208
-
209
- function extractStillImageMetadata(filePath: string): StillImageMetadata | null {
210
- if (extname(filePath).toLowerCase() !== ".png") return null;
211
-
212
- try {
213
- return extractPngMetadataFromBuffer(readFileSync(filePath));
214
- } catch {
215
- return null;
216
- }
217
- }
218
-
219
- /**
220
- * Read an ffprobe tag case-insensitively. ffmpeg/libavformat versions disagree
221
- * on tag casing — VP9 alpha is `alpha_mode` in older builds and `ALPHA_MODE`
222
- * in newer ones; HDR tags vary similarly. Use this for any sidecar tag where
223
- * you want to be resilient across muxer versions.
224
- */
225
- function readTagCI(tags: Record<string, string | undefined> | undefined, name: string): string {
226
- if (!tags) return "";
227
- const target = name.toLowerCase();
228
- for (const [key, value] of Object.entries(tags)) {
229
- if (key.toLowerCase() === target && typeof value === "string") return value;
230
- }
231
- return "";
232
- }
233
-
234
- function parseFrameRate(frameRateStr: string | undefined): number {
235
- if (!frameRateStr) return 0;
236
- const parts = frameRateStr.split("/");
237
- if (parts.length === 2) {
238
- const num = parseFloat(parts[0] ?? "");
239
- const den = parseFloat(parts[1] ?? "");
240
- if (den !== 0) return Math.round((num / den) * 100) / 100;
241
- }
242
- return parseFloat(frameRateStr) || 0;
243
- }
244
-
245
- /**
246
- * Probe a media file (video, image, or container) and return normalized metadata.
247
- *
248
- * Despite the legacy name `extractVideoMetadata` (still exported as a
249
- * deprecated alias below), this also handles still images such as PNG so it
250
- * can be used uniformly for any visual asset the HDR pipeline encounters.
251
- */
252
- export async function extractMediaMetadata(filePath: string): Promise<VideoMetadata> {
253
- const cached = videoMetadataCache.get(filePath);
254
- if (cached) return cached;
255
-
256
- const probePromise = (async (): Promise<VideoMetadata> => {
257
- const stillImageMeta = extractStillImageMetadata(filePath);
258
-
259
- let output: FFProbeOutput | null = null;
260
- try {
261
- const stdout = await runFfprobe([
262
- "-v",
263
- "quiet",
264
- "-print_format",
265
- "json",
266
- "-show_format",
267
- "-show_streams",
268
- filePath,
269
- ]);
270
- output = parseProbeJson(stdout);
271
- } catch (error) {
272
- if (!stillImageMeta) throw error;
273
- }
274
-
275
- const videoStream = output?.streams.find((s) => s.codec_type === "video");
276
- if (!videoStream) {
277
- if (stillImageMeta) {
278
- return {
279
- durationSeconds: 0,
280
- videoStreamDurationSeconds: 0,
281
- width: stillImageMeta.width,
282
- height: stillImageMeta.height,
283
- fps: 0,
284
- videoCodec: "png",
285
- hasAudio: false,
286
- isVFR: false,
287
- hasAlpha: false,
288
- colorSpace: stillImageMeta.colorSpace,
289
- };
290
- }
291
- throw new Error("[FFmpeg] No video stream found");
292
- }
293
-
294
- const rFps = parseFrameRate(videoStream.r_frame_rate);
295
- const avgFps = parseFrameRate(videoStream.avg_frame_rate);
296
- const fps = avgFps || rFps;
297
- // VFR: r_frame_rate (max/nominal) differs from avg_frame_rate (actual average) by >10%
298
- const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
299
-
300
- const colorTransfer = videoStream.color_transfer || "";
301
- const colorPrimaries = videoStream.color_primaries || "";
302
- const colorSpaceVal = videoStream.color_space || "";
303
- const ffprobeColorSpace =
304
- colorTransfer || colorPrimaries || colorSpaceVal
305
- ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal }
306
- : null;
307
- const colorSpace = ffprobeColorSpace ?? stillImageMeta?.colorSpace ?? null;
308
- const pixelFormat = videoStream.pix_fmt || "";
309
- const alphaMode = readTagCI(videoStream.tags, "alpha_mode");
310
- const hasAlpha =
311
- /(^|[^a-z])yuva|rgba|argb|bgra|gbrap|gray[a-z0-9]*a/i.test(pixelFormat) || alphaMode === "1";
312
-
313
- const containerDuration = output?.format.duration ? parseFloat(output.format.duration) : 0;
314
- const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
315
-
316
- return {
317
- durationSeconds: containerDuration,
318
- videoStreamDurationSeconds: streamDuration > 0 ? streamDuration : containerDuration,
319
- width: videoStream.width || stillImageMeta?.width || 0,
320
- height: videoStream.height || stillImageMeta?.height || 0,
321
- fps,
322
- videoCodec: videoStream.codec_name || "unknown",
323
- hasAudio: output?.streams.some((s) => s.codec_type === "audio") ?? false,
324
- isVFR,
325
- hasAlpha,
326
- colorSpace,
327
- };
328
- })();
329
-
330
- videoMetadataCache.set(filePath, probePromise);
331
- probePromise.catch(() => {
332
- if (videoMetadataCache.get(filePath) === probePromise) {
333
- videoMetadataCache.delete(filePath);
334
- }
335
- });
336
- return probePromise;
337
- }
338
-
339
- /**
340
- * @deprecated Use `extractMediaMetadata` — this name is kept for backward
341
- * compatibility with consumers that imported the original video-only name
342
- * before still-image (PNG) support was added. New callers should prefer
343
- * `extractMediaMetadata`.
344
- */
345
- export const extractVideoMetadata = extractMediaMetadata;
346
-
347
- export async function extractAudioMetadata(filePath: string): Promise<AudioMetadata> {
348
- const cached = audioMetadataCache.get(filePath);
349
- if (cached) return cached;
350
-
351
- const probePromise = (async (): Promise<AudioMetadata> => {
352
- const stdout = await runFfprobe([
353
- "-v",
354
- "quiet",
355
- "-print_format",
356
- "json",
357
- "-show_format",
358
- "-show_streams",
359
- filePath,
360
- ]);
361
- const output = parseProbeJson(stdout);
362
- const audioStream = output.streams.find((s) => s.codec_type === "audio");
363
- if (!audioStream) throw new Error("[FFmpeg] No audio stream found");
364
-
365
- const durationSeconds = output.format.duration ? parseFloat(output.format.duration) : 0;
366
-
367
- return {
368
- durationSeconds,
369
- sampleRate: audioStream.sample_rate ? parseInt(audioStream.sample_rate) : 44100,
370
- channels: audioStream.channels || 2,
371
- audioCodec: audioStream.codec_name || "unknown",
372
- bitrate: output.format.bit_rate ? parseInt(output.format.bit_rate) : undefined,
373
- };
374
- })();
375
-
376
- audioMetadataCache.set(filePath, probePromise);
377
- probePromise.catch(() => {
378
- if (audioMetadataCache.get(filePath) === probePromise) {
379
- audioMetadataCache.delete(filePath);
380
- }
381
- });
382
- return probePromise;
383
- }
384
-
385
- export interface KeyframeAnalysis {
386
- avgIntervalSeconds: number;
387
- maxIntervalSeconds: number;
388
- keyframeCount: number;
389
- isProblematic: boolean;
390
- }
391
-
392
- const keyframeCache = new Map<string, Promise<KeyframeAnalysis>>();
393
-
394
- /**
395
- * Check keyframe intervals in a video file. Intervals > 2s cause seeking
396
- * issues in the headless renderer and audio/video desync. Videos from
397
- * yt-dlp --download-sections or screen recordings often have sparse keyframes.
398
- */
399
- export async function analyzeKeyframeIntervals(filePath: string): Promise<KeyframeAnalysis> {
400
- const cached = keyframeCache.get(filePath);
401
- if (cached) return cached;
402
-
403
- const promise = analyzeKeyframeIntervalsUncached(filePath);
404
- keyframeCache.set(filePath, promise);
405
- promise.catch(() => {
406
- if (keyframeCache.get(filePath) === promise) {
407
- keyframeCache.delete(filePath);
408
- }
409
- });
410
- return promise;
411
- }
412
-
413
- async function analyzeKeyframeIntervalsUncached(filePath: string): Promise<KeyframeAnalysis> {
414
- const stdout = await runFfprobe([
415
- "-v",
416
- "quiet",
417
- "-select_streams",
418
- "v:0",
419
- "-skip_frame",
420
- "nokey",
421
- "-show_entries",
422
- "frame=pts_time",
423
- "-of",
424
- "csv=p=0",
425
- filePath,
426
- ]);
427
-
428
- const timestamps = stdout
429
- .split("\n")
430
- .map((line) => parseFloat(line.trim()))
431
- .filter((t) => Number.isFinite(t));
432
-
433
- if (timestamps.length < 2) {
434
- return {
435
- avgIntervalSeconds: 0,
436
- maxIntervalSeconds: 0,
437
- keyframeCount: timestamps.length,
438
- isProblematic: false,
439
- };
440
- }
441
-
442
- let maxInterval = 0;
443
- let totalInterval = 0;
444
- for (let i = 1; i < timestamps.length; i++) {
445
- const interval = (timestamps[i] ?? 0) - (timestamps[i - 1] ?? 0);
446
- totalInterval += interval;
447
- if (interval > maxInterval) maxInterval = interval;
448
- }
449
-
450
- const avgInterval = totalInterval / (timestamps.length - 1);
451
- return {
452
- avgIntervalSeconds: Math.round(avgInterval * 100) / 100,
453
- maxIntervalSeconds: Math.round(maxInterval * 100) / 100,
454
- keyframeCount: timestamps.length,
455
- isProblematic: maxInterval > 2,
456
- };
457
- }
@@ -1,140 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
-
3
- import {
4
- getCompiledGpuEncoders,
5
- getGpuEncoderName,
6
- getProbeArgs,
7
- mapPresetForGpuEncoder,
8
- selectUsableGpuEncoder,
9
- } from "./gpuEncoder.js";
10
-
11
- afterEach(() => {
12
- vi.useRealTimers();
13
- });
14
-
15
- describe("getCompiledGpuEncoders", () => {
16
- it("recognizes AMD AMF in FFmpeg's encoder list", () => {
17
- expect(
18
- getCompiledGpuEncoders(`
19
- V....D h264_nvenc NVIDIA NVENC H.264 encoder
20
- V....D h264_amf AMD AMF H.264 Encoder
21
- V....D h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video)
22
- `),
23
- ).toEqual(["nvenc", "qsv", "amf"]);
24
- });
25
- });
26
-
27
- describe("selectUsableGpuEncoder", () => {
28
- it("runs probe checks concurrently while preserving candidate priority", async () => {
29
- vi.useFakeTimers();
30
- const started: string[] = [];
31
- const usable = selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
32
- started.push(encoder);
33
- await new Promise((resolve) => setTimeout(resolve, encoder === "nvenc" ? 50 : 1));
34
- return true;
35
- });
36
-
37
- await vi.advanceTimersByTimeAsync(1);
38
- expect(started).toEqual(["nvenc", "amf"]);
39
-
40
- await vi.advanceTimersByTimeAsync(49);
41
- expect(await usable).toBe("nvenc");
42
- });
43
-
44
- it("falls through from compiled-but-unusable NVENC to usable AMD AMF", async () => {
45
- const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
46
- return encoder === "amf";
47
- });
48
-
49
- expect(usable).toBe("amf");
50
- });
51
-
52
- it("treats rejected probe checks as unusable", async () => {
53
- const usable = await selectUsableGpuEncoder(["nvenc", "amf"], async (encoder) => {
54
- if (encoder === "nvenc") {
55
- throw new Error("driver probe failed");
56
- }
57
- return encoder === "amf";
58
- });
59
-
60
- expect(usable).toBe("amf");
61
- });
62
- });
63
-
64
- describe("getGpuEncoderName", () => {
65
- it("maps AMD AMF to FFmpeg's h264 and hevc encoder names", () => {
66
- expect(getGpuEncoderName("amf", "h264")).toBe("h264_amf");
67
- expect(getGpuEncoderName("amf", "h265")).toBe("hevc_amf");
68
- });
69
- });
70
-
71
- describe("mapPresetForGpuEncoder", () => {
72
- describe("nvenc", () => {
73
- it.each([
74
- ["ultrafast", "p1"],
75
- ["superfast", "p1"],
76
- ["veryfast", "p2"],
77
- ["faster", "p3"],
78
- ["fast", "p4"],
79
- ["medium", "p4"],
80
- ["slow", "p5"],
81
- ["slower", "p6"],
82
- ["veryslow", "p7"],
83
- ["placebo", "p7"],
84
- ])("maps libx264 preset %s to NVENC %s", (input, expected) => {
85
- expect(mapPresetForGpuEncoder("nvenc", input)).toBe(expected);
86
- });
87
-
88
- it.each(["p1", "p2", "p3", "p4", "p5", "p6", "p7"])(
89
- "passes NVENC-native preset %s through unchanged",
90
- (preset) => {
91
- expect(mapPresetForGpuEncoder("nvenc", preset)).toBe(preset);
92
- },
93
- );
94
-
95
- it("falls back to p4 for unknown preset values", () => {
96
- expect(mapPresetForGpuEncoder("nvenc", "nonsense")).toBe("p4");
97
- });
98
- });
99
-
100
- describe("qsv", () => {
101
- it.each([
102
- ["ultrafast", "veryfast"],
103
- ["superfast", "veryfast"],
104
- ["placebo", "veryslow"],
105
- ])("rewrites libx264-only preset %s to QSV-supported %s", (input, expected) => {
106
- expect(mapPresetForGpuEncoder("qsv", input)).toBe(expected);
107
- });
108
-
109
- it.each(["veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"])(
110
- "passes supported preset %s through unchanged",
111
- (preset) => {
112
- expect(mapPresetForGpuEncoder("qsv", preset)).toBe(preset);
113
- },
114
- );
115
- });
116
-
117
- describe("other encoders", () => {
118
- it.each(["videotoolbox", "vaapi", "amf"] as const)(
119
- "passes preset through unchanged for %s",
120
- (encoder) => {
121
- expect(mapPresetForGpuEncoder(encoder, "medium")).toBe("medium");
122
- expect(mapPresetForGpuEncoder(encoder, "ultrafast")).toBe("ultrafast");
123
- },
124
- );
125
-
126
- it("passes preset through unchanged when encoder is null (CPU)", () => {
127
- expect(mapPresetForGpuEncoder(null, "ultrafast")).toBe("ultrafast");
128
- });
129
- });
130
- });
131
-
132
- describe("getProbeArgs", () => {
133
- it("uses 320x240 probe dimensions for all GPU encoders", () => {
134
- const encoders = ["nvenc", "videotoolbox", "vaapi", "qsv", "amf"] as const;
135
- for (const encoder of encoders) {
136
- const args = getProbeArgs(encoder);
137
- expect(args).toContain("color=size=320x240:rate=1:duration=1");
138
- }
139
- });
140
- });