@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.
- package/package.json +24 -7
- package/scripts/generate-lut-reference.py +0 -168
- package/scripts/test-fitTextFontSize-browser.ts +0 -135
- package/src/cdp-headless-experimental.d.ts +0 -54
- package/src/config.test.ts +0 -213
- package/src/config.ts +0 -417
- package/src/index.ts +0 -273
- package/src/services/audioMixer.test.ts +0 -326
- package/src/services/audioMixer.ts +0 -604
- package/src/services/audioMixer.types.ts +0 -35
- package/src/services/audioVolumeEnvelope.test.ts +0 -176
- package/src/services/audioVolumeEnvelope.ts +0 -138
- package/src/services/browserManager.test.ts +0 -330
- package/src/services/browserManager.ts +0 -670
- package/src/services/chunkEncoder.test.ts +0 -1415
- package/src/services/chunkEncoder.ts +0 -831
- package/src/services/chunkEncoder.types.ts +0 -60
- package/src/services/extractionCache.test.ts +0 -199
- package/src/services/extractionCache.ts +0 -216
- package/src/services/fileServer.ts +0 -110
- package/src/services/frameCapture-discardWarmup.test.ts +0 -183
- package/src/services/frameCapture-namePolyfill.test.ts +0 -78
- package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
- package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
- package/src/services/frameCapture-warmupTicks.test.ts +0 -174
- package/src/services/frameCapture.test.ts +0 -192
- package/src/services/frameCapture.ts +0 -1934
- package/src/services/hdrCapture.test.ts +0 -159
- package/src/services/hdrCapture.ts +0 -315
- package/src/services/parallelCoordinator.test.ts +0 -139
- package/src/services/parallelCoordinator.ts +0 -437
- package/src/services/screenshotService.test.ts +0 -510
- package/src/services/screenshotService.ts +0 -615
- package/src/services/streamingEncoder.test.ts +0 -832
- package/src/services/streamingEncoder.ts +0 -594
- package/src/services/systemMemory.test.ts +0 -324
- package/src/services/systemMemory.ts +0 -180
- package/src/services/videoFrameExtractor.test.ts +0 -1062
- package/src/services/videoFrameExtractor.ts +0 -1139
- package/src/services/videoFrameInjector.test.ts +0 -300
- package/src/services/videoFrameInjector.ts +0 -687
- package/src/services/vp9Options.ts +0 -13
- package/src/types.ts +0 -191
- package/src/utils/alphaBlit.test.ts +0 -1349
- package/src/utils/alphaBlit.ts +0 -1015
- package/src/utils/assertSwiftShader.test.ts +0 -130
- package/src/utils/assertSwiftShader.ts +0 -126
- package/src/utils/ffmpegBinaries.test.ts +0 -43
- package/src/utils/ffmpegBinaries.ts +0 -63
- package/src/utils/ffprobe.test.ts +0 -342
- package/src/utils/ffprobe.ts +0 -457
- package/src/utils/gpuEncoder.test.ts +0 -140
- package/src/utils/gpuEncoder.ts +0 -268
- package/src/utils/hdr.test.ts +0 -191
- package/src/utils/hdr.ts +0 -137
- package/src/utils/hdrCompositing.test.ts +0 -130
- package/src/utils/htmlTemplate.test.ts +0 -42
- package/src/utils/htmlTemplate.ts +0 -42
- package/src/utils/layerCompositor.test.ts +0 -150
- package/src/utils/layerCompositor.ts +0 -58
- package/src/utils/parityContract.ts +0 -1
- package/src/utils/processTracker.test.ts +0 -74
- package/src/utils/processTracker.ts +0 -41
- package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
- package/src/utils/runFfmpeg.test.ts +0 -102
- package/src/utils/runFfmpeg.ts +0 -136
- package/src/utils/shaderTransitions.test.ts +0 -738
- package/src/utils/shaderTransitions.ts +0 -1130
- package/src/utils/uint16-alignment-audit.test.ts +0 -125
- package/src/utils/urlDownloader.test.ts +0 -65
- package/src/utils/urlDownloader.ts +0 -143
- package/tsconfig.json +0 -19
- 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
|
-
}
|