@hyperframes/engine 0.6.118 → 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,437 +0,0 @@
1
- /**
2
- * Parallel Coordinator Service
3
- *
4
- * Coordinates parallel frame capture across multiple Puppeteer sessions.
5
- * Auto-detects optimal worker count based on CPU/memory.
6
- */
7
-
8
- import { cpus, freemem } from "os";
9
- import { existsSync, mkdirSync, readdirSync } from "fs";
10
- import { copyFile, rename } from "fs/promises";
11
- import { join } from "path";
12
-
13
- import {
14
- createCaptureSession,
15
- initializeSession,
16
- closeCaptureSession,
17
- captureFrame,
18
- captureFrameToBuffer,
19
- getCapturePerfSummary,
20
- type CaptureSession,
21
- type CaptureOptions,
22
- type CapturePerfSummary,
23
- type BeforeCaptureHook,
24
- } from "./frameCapture.js";
25
- import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
26
- import { assertSwiftShader } from "../utils/assertSwiftShader.js";
27
- import { readWebGlVendorInfoFromCanvas } from "../utils/readWebGlVendorInfoFromCanvas.js";
28
- import { resolveHeadlessShellPath } from "./browserManager.js";
29
- import { getSystemTotalMb } from "./systemMemory.js";
30
-
31
- export interface WorkerTask {
32
- workerId: number;
33
- startFrame: number;
34
- endFrame: number;
35
- outputDir: string;
36
- /**
37
- * Offset subtracted from the absolute frame index when naming the captured
38
- * file (`frame_<i - outputFrameOffset>.{ext}`). Default 0. Distributed
39
- * chunks set this to the chunk's absolute startFrame so file names land
40
- * 0-indexed within the chunk's range — the encoder reads frames
41
- * sequentially without an `-start_number` override. The per-frame TIME
42
- * calculation still uses the absolute frame index.
43
- */
44
- outputFrameOffset?: number;
45
- }
46
-
47
- export interface WorkerResult {
48
- workerId: number;
49
- framesCaptured: number;
50
- startFrame: number;
51
- endFrame: number;
52
- durationMs: number;
53
- perf?: CapturePerfSummary;
54
- error?: string;
55
- diagnostics?: string[];
56
- }
57
-
58
- export interface ParallelProgress {
59
- totalFrames: number;
60
- capturedFrames: number;
61
- activeWorkers: number;
62
- workerProgress: Map<number, number>;
63
- }
64
-
65
- export interface WorkerSizingConfig extends Partial<
66
- Pick<
67
- EngineConfig,
68
- "concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold"
69
- >
70
- > {
71
- /**
72
- * Relative per-frame capture cost for auto worker sizing. Values above 1
73
- * represent compositions that put more CPU pressure on each Chrome worker
74
- * than a plain DOM screenshot. Explicit --workers requests ignore this hint.
75
- */
76
- captureCostMultiplier?: number;
77
- }
78
-
79
- const MEMORY_PER_WORKER_MB = 256;
80
- const MIN_WORKERS = 1;
81
- const MAX_WORKER_DIAGNOSTIC_LINES = 8;
82
- // Hard ceiling on explicit `--workers N` requests. Above this, the cost of
83
- // CDP-protocol dispatch through Node's main event loop and OS scheduling
84
- // noise overwhelms any further parallelism. Bumped from 10 → 24 in hf#732
85
- // follow-up so high-core hosts (32-96+ cores) can actually surface the
86
- // hardware to renders that are CPU-bound on DOM capture.
87
- const ABSOLUTE_MAX_WORKERS = 24;
88
- // `auto` concurrency picks this many workers as the upper bound. Bumped
89
- // from a hardcoded 6 → CPU-scaled value (floor(cpuCount/8), floor at 6,
90
- // ceiling at 16) in hf#732 follow-up. Rationale: the prior fixed cap of 6
91
- // left ~90 cores idle on the validation host and forced users to pass
92
- // `--workers N` to opt in. Now `auto` matches what a thoughtful operator
93
- // would pick by hand. The /8 divisor leaves headroom for each Chrome
94
- // worker's SwiftShader compositor + the shader-blend thread pool, both of
95
- // which are themselves CPU-heavy.
96
- function defaultSafeMaxWorkers(): number {
97
- return Math.max(6, Math.min(16, Math.floor(cpus().length / 8)));
98
- }
99
- const MIN_FRAMES_PER_WORKER = 30;
100
-
101
- export function selectWorkerDiagnostics(
102
- lines: readonly string[],
103
- maxLines: number = MAX_WORKER_DIAGNOSTIC_LINES,
104
- ): string[] {
105
- return lines
106
- .filter((line) =>
107
- /\[(FrameCapture:ERROR|Browser:ERROR|Browser:PAGEERROR|Browser:REQUESTFAILED|Browser:HTTP\d{3})\]/.test(
108
- line,
109
- ),
110
- )
111
- .slice(-maxLines);
112
- }
113
-
114
- function compactDiagnosticLine(line: string): string {
115
- return line.replace(/\s+/g, " ").trim();
116
- }
117
-
118
- export function formatWorkerFailure(result: WorkerResult): string {
119
- const base = `Worker ${result.workerId}: ${result.error ?? "unknown error"}`;
120
- if (!result.diagnostics || result.diagnostics.length === 0) return base;
121
-
122
- const diagnostics = result.diagnostics.map(compactDiagnosticLine).join(" | ");
123
- return `${base}; diagnostics: ${diagnostics}`;
124
- }
125
-
126
- export function calculateOptimalWorkers(
127
- totalFrames: number,
128
- requested?: number,
129
- config?: WorkerSizingConfig,
130
- ): number {
131
- // Resolve effective values: config overrides → DEFAULT_CONFIG fallback.
132
- const effectiveMaxWorkers = (() => {
133
- const concurrency = config?.concurrency ?? DEFAULT_CONFIG.concurrency;
134
- if (concurrency !== "auto") {
135
- return Math.max(MIN_WORKERS, Math.min(ABSOLUTE_MAX_WORKERS, Math.floor(concurrency)));
136
- }
137
- return defaultSafeMaxWorkers();
138
- })();
139
- const effectiveCoresPerWorker = config?.coresPerWorker ?? DEFAULT_CONFIG.coresPerWorker;
140
- const effectiveMinParallelFrames = config?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
141
- const effectiveLargeRenderThreshold =
142
- config?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
143
- const captureCostMultiplier = Math.max(1, config?.captureCostMultiplier ?? 1);
144
-
145
- if (requested !== undefined) {
146
- return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
147
- }
148
-
149
- if (totalFrames < MIN_FRAMES_PER_WORKER * 2) return 1;
150
-
151
- const cpuCount = cpus().length;
152
- const cpuBasedWorkers = Math.max(1, cpuCount - 2);
153
-
154
- // Use total memory instead of free memory — macOS reports misleadingly low
155
- // freemem() because it aggressively caches files in "inactive" memory that
156
- // is immediately reclaimable.
157
- const totalMemoryMB = getSystemTotalMb();
158
- const memoryBasedWorkers = Math.max(1, Math.floor((totalMemoryMB * 0.5) / MEMORY_PER_WORKER_MB));
159
-
160
- const frameBasedWorkers = Math.floor(totalFrames / MIN_FRAMES_PER_WORKER);
161
-
162
- const optimal = Math.min(cpuBasedWorkers, memoryBasedWorkers, frameBasedWorkers);
163
- const minWorkersForJob = totalFrames >= effectiveMinParallelFrames ? 2 : MIN_WORKERS;
164
- let finalWorkers = Math.max(minWorkersForJob, Math.min(effectiveMaxWorkers, optimal));
165
-
166
- // Adaptive scaling: cap workers for large or expensive renders to prevent
167
- // CPU contention. Each Chrome process (with SwiftShader) is CPU-heavy; too
168
- // many concurrent captures can starve the compositor and surface as CDP
169
- // protocol timeouts. Scale proportionally to CPU count and composition cost:
170
- // 8 cores → 2 workers, 16 cores → 5 workers, 32 cores → 10 workers.
171
- const weightedFrames = totalFrames * captureCostMultiplier;
172
- const contentionThreshold = Math.max(
173
- effectiveMinParallelFrames,
174
- Math.floor(effectiveLargeRenderThreshold / 3),
175
- );
176
- if (totalFrames >= effectiveLargeRenderThreshold || weightedFrames >= contentionThreshold) {
177
- const weightedCoresPerWorker = effectiveCoresPerWorker * captureCostMultiplier;
178
- const cpuScaledMax = Math.max(MIN_WORKERS, Math.floor(cpuCount / weightedCoresPerWorker));
179
- if (finalWorkers > cpuScaledMax) {
180
- finalWorkers = cpuScaledMax;
181
- }
182
- }
183
-
184
- return finalWorkers;
185
- }
186
-
187
- export function distributeFrames(
188
- totalFrames: number,
189
- workerCount: number,
190
- workDir: string,
191
- rangeStart: number = 0,
192
- ): WorkerTask[] {
193
- const tasks: WorkerTask[] = [];
194
- const framesPerWorker = Math.ceil(totalFrames / workerCount);
195
-
196
- for (let i = 0; i < workerCount; i++) {
197
- const startFrame = rangeStart + i * framesPerWorker;
198
- const endFrame = Math.min(rangeStart + (i + 1) * framesPerWorker, rangeStart + totalFrames);
199
- if (startFrame >= rangeStart + totalFrames) break;
200
-
201
- tasks.push({
202
- workerId: i,
203
- startFrame,
204
- endFrame,
205
- outputDir: join(workDir, `worker-${i}`),
206
- outputFrameOffset: rangeStart,
207
- });
208
- }
209
-
210
- return tasks;
211
- }
212
-
213
- /**
214
- * Decide whether a parallel worker should run the per-worker SwiftShader
215
- * assertion. Gated to worker 0 only: workers within a chunk share the same
216
- * Chrome binary, flags, and OS/driver state, so one verification per chunk
217
- * is sufficient. See `heygen-com/hyperframes#955`.
218
- */
219
- export function shouldVerifyWorkerGpu(workerId: number, config?: Partial<EngineConfig>): boolean {
220
- return config?.browserGpuMode === "software" && workerId === 0;
221
- }
222
-
223
- async function captureFrameRange(
224
- session: CaptureSession,
225
- task: WorkerTask,
226
- captureOptions: CaptureOptions,
227
- signal: AbortSignal | undefined,
228
- onFrameCaptured: ((workerId: number, frameIndex: number) => void) | undefined,
229
- onFrameBuffer: ((frameIndex: number, buffer: Buffer) => Promise<void>) | undefined,
230
- ): Promise<number> {
231
- let framesCaptured = 0;
232
- const outputOffset = task.outputFrameOffset ?? 0;
233
- for (let i = task.startFrame; i < task.endFrame; i++) {
234
- if (signal?.aborted) throw new Error("Parallel worker cancelled");
235
- const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
236
- const fileFrameIdx = i - outputOffset;
237
-
238
- if (onFrameBuffer) {
239
- const { buffer } = await captureFrameToBuffer(session, fileFrameIdx, time);
240
- await onFrameBuffer(i, buffer);
241
- } else {
242
- await captureFrame(session, fileFrameIdx, time);
243
- }
244
- framesCaptured++;
245
- if (onFrameCaptured) onFrameCaptured(task.workerId, i);
246
- }
247
- return framesCaptured;
248
- }
249
-
250
- async function executeWorkerTask(
251
- task: WorkerTask,
252
- serverUrl: string,
253
- captureOptions: CaptureOptions,
254
- createBeforeCaptureHook: () => BeforeCaptureHook | null,
255
- signal?: AbortSignal,
256
- onFrameCaptured?: (workerId: number, frameIndex: number) => void,
257
- onFrameBuffer?: (frameIndex: number, buffer: Buffer) => Promise<void>,
258
- config?: Partial<EngineConfig>,
259
- parallel?: boolean,
260
- ): Promise<WorkerResult> {
261
- const startTime = Date.now();
262
- let framesCaptured = 0;
263
-
264
- if (!existsSync(task.outputDir)) mkdirSync(task.outputDir, { recursive: true });
265
-
266
- let session: CaptureSession | null = null;
267
- let perf: CapturePerfSummary | undefined;
268
-
269
- // BeginFrame's compositor is process-global — multiple pages driving
270
- // beginFrame in the same browser race it and crash with "Target closed".
271
- // Only disable the pool when BeginFrame mode would actually be active.
272
- // Must match the predicate in createCaptureSession (frameCapture.ts):
273
- // Linux + headless-shell + !forceScreenshot + !supersampling.
274
- const supersampling = (captureOptions.deviceScaleFactor ?? 1) > 1;
275
- const needsSeparateBrowsers =
276
- parallel &&
277
- process.platform === "linux" &&
278
- !config?.forceScreenshot &&
279
- !supersampling &&
280
- resolveHeadlessShellPath(config) !== undefined;
281
- const workerConfig: Partial<EngineConfig> | undefined = needsSeparateBrowsers
282
- ? { ...config, enableBrowserPool: false }
283
- : config;
284
-
285
- try {
286
- session = await createCaptureSession(
287
- serverUrl,
288
- task.outputDir,
289
- captureOptions,
290
- createBeforeCaptureHook(),
291
- workerConfig,
292
- );
293
- // Worker-0-only SwiftShader assertion — see `shouldVerifyWorkerGpu` and #955.
294
- if (shouldVerifyWorkerGpu(task.workerId, workerConfig)) {
295
- await assertSwiftShader(session.page, readWebGlVendorInfoFromCanvas);
296
- }
297
- await initializeSession(session);
298
- framesCaptured = await captureFrameRange(
299
- session,
300
- task,
301
- captureOptions,
302
- signal,
303
- onFrameCaptured,
304
- onFrameBuffer,
305
- );
306
-
307
- perf = getCapturePerfSummary(session);
308
- return {
309
- workerId: task.workerId,
310
- framesCaptured,
311
- startFrame: task.startFrame,
312
- endFrame: task.endFrame,
313
- durationMs: Date.now() - startTime,
314
- perf,
315
- };
316
- } catch (error) {
317
- const errMsg = error instanceof Error ? error.message : String(error);
318
- const diagnostics = session ? selectWorkerDiagnostics(session.browserConsoleBuffer) : [];
319
- return {
320
- workerId: task.workerId,
321
- framesCaptured,
322
- startFrame: task.startFrame,
323
- endFrame: task.endFrame,
324
- durationMs: Date.now() - startTime,
325
- perf,
326
- error: errMsg,
327
- diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
328
- };
329
- } finally {
330
- if (session) await closeCaptureSession(session).catch(() => {});
331
- }
332
- }
333
-
334
- export async function executeParallelCapture(
335
- serverUrl: string,
336
- workDir: string,
337
- tasks: WorkerTask[],
338
- captureOptions: CaptureOptions,
339
- createBeforeCaptureHook: () => BeforeCaptureHook | null,
340
- signal?: AbortSignal,
341
- onProgress?: (progress: ParallelProgress) => void,
342
- onFrameBuffer?: (frameIndex: number, buffer: Buffer) => Promise<void>,
343
- config?: Partial<EngineConfig>,
344
- ): Promise<WorkerResult[]> {
345
- const totalFrames = tasks.reduce((sum, t) => sum + (t.endFrame - t.startFrame), 0);
346
- const workerProgress = new Map<number, number>();
347
-
348
- for (const task of tasks) workerProgress.set(task.workerId, 0);
349
-
350
- const onFrameCaptured = (workerId: number, _frameIndex: number) => {
351
- const current = workerProgress.get(workerId) || 0;
352
- workerProgress.set(workerId, current + 1);
353
-
354
- if (onProgress) {
355
- const capturedFrames = Array.from(workerProgress.values()).reduce((a, b) => a + b, 0);
356
- onProgress({
357
- totalFrames,
358
- capturedFrames,
359
- activeWorkers: tasks.length,
360
- workerProgress: new Map(workerProgress),
361
- });
362
- }
363
- };
364
-
365
- const parallel = tasks.length > 1;
366
- const results = await Promise.all(
367
- tasks.map((task) =>
368
- executeWorkerTask(
369
- task,
370
- serverUrl,
371
- captureOptions,
372
- createBeforeCaptureHook,
373
- signal,
374
- onFrameCaptured,
375
- onFrameBuffer,
376
- config,
377
- parallel,
378
- ),
379
- ),
380
- );
381
-
382
- const errors = results.filter((r) => r.error);
383
- if (errors.length > 0) {
384
- const errorMessages = errors.map(formatWorkerFailure).join("; ");
385
- throw new Error(`[Parallel] Capture failed: ${errorMessages}`);
386
- }
387
-
388
- return results;
389
- }
390
-
391
- export async function mergeWorkerFrames(
392
- workDir: string,
393
- tasks: WorkerTask[],
394
- outputDir: string,
395
- ): Promise<number> {
396
- if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
397
-
398
- let totalFrames = 0;
399
- const sortedTasks = [...tasks].sort((a, b) => a.startFrame - b.startFrame);
400
-
401
- for (const task of sortedTasks) {
402
- if (!existsSync(task.outputDir)) {
403
- continue;
404
- }
405
-
406
- const files = readdirSync(task.outputDir)
407
- .filter((f) => f.startsWith("frame_") && (f.endsWith(".jpg") || f.endsWith(".png")))
408
- .sort();
409
- const copyTasks = files.map(async (file) => {
410
- const sourcePath = join(task.outputDir, file);
411
- const targetPath = join(outputDir, file);
412
- try {
413
- await rename(sourcePath, targetPath);
414
- } catch {
415
- await copyFile(sourcePath, targetPath);
416
- }
417
- });
418
- await Promise.all(copyTasks);
419
- totalFrames += files.length;
420
- }
421
-
422
- return totalFrames;
423
- }
424
-
425
- export function getSystemResources(): {
426
- cpuCores: number;
427
- totalMemoryMB: number;
428
- freeMemoryMB: number;
429
- recommendedWorkers: number;
430
- } {
431
- return {
432
- cpuCores: cpus().length,
433
- totalMemoryMB: getSystemTotalMb(),
434
- freeMemoryMB: Math.round(freemem() / (1024 * 1024)),
435
- recommendedWorkers: calculateOptimalWorkers(1000),
436
- };
437
- }