@hyperframes/producer 0.5.0-alpha.5 → 0.5.0-alpha.7
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/dist/index.js +470 -30
- package/dist/index.js.map +2 -2
- package/dist/public-server.js +470 -30
- package/dist/public-server.js.map +2 -2
- package/dist/services/htmlCompiler.d.ts +2 -0
- package/dist/services/htmlCompiler.d.ts.map +1 -1
- package/dist/services/renderOrchestrator.d.ts +45 -4
- package/dist/services/renderOrchestrator.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -100569,7 +100569,7 @@ var captionRules = [
|
|
|
100569
100569
|
const hasHardKill = /\.set\s*\([^,]+,\s*\{[^}]*(?:visibility\s*:\s*["']hidden["']|opacity\s*:\s*0)/.test(
|
|
100570
100570
|
content
|
|
100571
100571
|
);
|
|
100572
|
-
const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /
|
|
100572
|
+
const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /karaoke|caption[-_]?(?:group|word|line|block)|cg-/.test(content);
|
|
100573
100573
|
if (hasCaptionLoop && hasExitTween && !hasHardKill) {
|
|
100574
100574
|
findings.push({
|
|
100575
100575
|
code: "caption_exit_missing_hard_kill",
|
|
@@ -104264,6 +104264,7 @@ function calculateOptimalWorkers(totalFrames, requested, config2) {
|
|
|
104264
104264
|
const effectiveCoresPerWorker = config2?.coresPerWorker ?? DEFAULT_CONFIG.coresPerWorker;
|
|
104265
104265
|
const effectiveMinParallelFrames = config2?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
|
|
104266
104266
|
const effectiveLargeRenderThreshold = config2?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
|
|
104267
|
+
const captureCostMultiplier = Math.max(1, config2?.captureCostMultiplier ?? 1);
|
|
104267
104268
|
if (requested !== void 0) {
|
|
104268
104269
|
return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
|
|
104269
104270
|
}
|
|
@@ -104276,8 +104277,14 @@ function calculateOptimalWorkers(totalFrames, requested, config2) {
|
|
|
104276
104277
|
const optimal = Math.min(cpuBasedWorkers, memoryBasedWorkers, frameBasedWorkers);
|
|
104277
104278
|
const minWorkersForJob = totalFrames >= effectiveMinParallelFrames ? 2 : MIN_WORKERS;
|
|
104278
104279
|
let finalWorkers = Math.max(minWorkersForJob, Math.min(effectiveMaxWorkers, optimal));
|
|
104279
|
-
|
|
104280
|
-
|
|
104280
|
+
const weightedFrames = totalFrames * captureCostMultiplier;
|
|
104281
|
+
const contentionThreshold = Math.max(
|
|
104282
|
+
effectiveMinParallelFrames,
|
|
104283
|
+
Math.floor(effectiveLargeRenderThreshold / 3)
|
|
104284
|
+
);
|
|
104285
|
+
if (totalFrames >= effectiveLargeRenderThreshold || weightedFrames >= contentionThreshold) {
|
|
104286
|
+
const weightedCoresPerWorker = effectiveCoresPerWorker * captureCostMultiplier;
|
|
104287
|
+
const cpuScaledMax = Math.max(MIN_WORKERS, Math.floor(cpuCount / weightedCoresPerWorker));
|
|
104281
104288
|
if (finalWorkers > cpuScaledMax) {
|
|
104282
104289
|
finalWorkers = cpuScaledMax;
|
|
104283
104290
|
}
|
|
@@ -109423,6 +109430,18 @@ function detectRenderModeHints(html) {
|
|
|
109423
109430
|
reasons
|
|
109424
109431
|
};
|
|
109425
109432
|
}
|
|
109433
|
+
var SHADER_TRANSITION_USAGE_PATTERN = /\b(?:(?:window|globalThis)\s*\.\s*)?HyperShader\s*\.\s*init\s*\(|\b__hf\s*\.\s*transitions\s*=/;
|
|
109434
|
+
function detectShaderTransitionUsage(html) {
|
|
109435
|
+
let scriptMatch;
|
|
109436
|
+
const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
|
|
109437
|
+
while ((scriptMatch = scriptPattern.exec(html)) !== null) {
|
|
109438
|
+
const attrs = scriptMatch[1] || "";
|
|
109439
|
+
if (/\bsrc\s*=/i.test(attrs)) continue;
|
|
109440
|
+
const content = stripJsComments(stripCompilerMountBootstrap(scriptMatch[2] || ""));
|
|
109441
|
+
if (SHADER_TRANSITION_USAGE_PATTERN.test(content)) return true;
|
|
109442
|
+
}
|
|
109443
|
+
return false;
|
|
109444
|
+
}
|
|
109426
109445
|
async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
|
|
109427
109446
|
let filePath = src;
|
|
109428
109447
|
if (isHttpUrl(src)) {
|
|
@@ -109959,6 +109978,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
109959
109978
|
"$1"
|
|
109960
109979
|
);
|
|
109961
109980
|
const renderModeHints = detectRenderModeHints(sanitizedHtml);
|
|
109981
|
+
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
109962
109982
|
const coalescedHtml = await injectDeterministicFontFaces(
|
|
109963
109983
|
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
|
|
109964
109984
|
);
|
|
@@ -110006,7 +110026,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
110006
110026
|
width,
|
|
110007
110027
|
height,
|
|
110008
110028
|
staticDuration,
|
|
110009
|
-
renderModeHints
|
|
110029
|
+
renderModeHints,
|
|
110030
|
+
hasShaderTransitions
|
|
110010
110031
|
};
|
|
110011
110032
|
}
|
|
110012
110033
|
async function discoverMediaFromBrowser(page) {
|
|
@@ -110108,7 +110129,8 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
|
|
|
110108
110129
|
audios,
|
|
110109
110130
|
images,
|
|
110110
110131
|
unresolvedCompositions: remaining,
|
|
110111
|
-
renderModeHints: compiled.renderModeHints
|
|
110132
|
+
renderModeHints: compiled.renderModeHints,
|
|
110133
|
+
hasShaderTransitions: compiled.hasShaderTransitions
|
|
110112
110134
|
};
|
|
110113
110135
|
}
|
|
110114
110136
|
|
|
@@ -110387,7 +110409,8 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
|
|
|
110387
110409
|
mediaStart: a.mediaStart
|
|
110388
110410
|
})),
|
|
110389
110411
|
subCompositions: Array.from(compiled.subCompositions.keys()),
|
|
110390
|
-
renderModeHints: compiled.renderModeHints
|
|
110412
|
+
renderModeHints: compiled.renderModeHints,
|
|
110413
|
+
hasShaderTransitions: compiled.hasShaderTransitions
|
|
110391
110414
|
};
|
|
110392
110415
|
writeFileSync5(join16(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
110393
110416
|
}
|
|
@@ -110400,6 +110423,290 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
|
|
|
110400
110423
|
reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
|
|
110401
110424
|
});
|
|
110402
110425
|
}
|
|
110426
|
+
function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log = defaultLogger, measuredCaptureCost) {
|
|
110427
|
+
const captureCost = combineCaptureCostEstimates(
|
|
110428
|
+
estimateCaptureCostMultiplier(compiled, composition),
|
|
110429
|
+
measuredCaptureCost
|
|
110430
|
+
);
|
|
110431
|
+
const workerCount = calculateOptimalWorkers(totalFrames, requestedWorkers, {
|
|
110432
|
+
...cfg,
|
|
110433
|
+
captureCostMultiplier: captureCost.multiplier
|
|
110434
|
+
});
|
|
110435
|
+
if (requestedWorkers !== void 0 || captureCost.multiplier <= 1) {
|
|
110436
|
+
return workerCount;
|
|
110437
|
+
}
|
|
110438
|
+
const baselineWorkers = calculateOptimalWorkers(totalFrames, void 0, cfg);
|
|
110439
|
+
if (workerCount < baselineWorkers) {
|
|
110440
|
+
log.warn(
|
|
110441
|
+
"[Render] Reduced auto worker count for high-cost capture workload to avoid Chrome compositor starvation.",
|
|
110442
|
+
{
|
|
110443
|
+
from: baselineWorkers,
|
|
110444
|
+
to: workerCount,
|
|
110445
|
+
costMultiplier: captureCost.multiplier,
|
|
110446
|
+
reasons: captureCost.reasons
|
|
110447
|
+
}
|
|
110448
|
+
);
|
|
110449
|
+
}
|
|
110450
|
+
return workerCount;
|
|
110451
|
+
}
|
|
110452
|
+
function estimateCaptureCostMultiplier(compiled, composition) {
|
|
110453
|
+
let multiplier = 1;
|
|
110454
|
+
const reasons = [];
|
|
110455
|
+
if (compiled.hasShaderTransitions) {
|
|
110456
|
+
multiplier += 2;
|
|
110457
|
+
reasons.push("shader-transitions");
|
|
110458
|
+
}
|
|
110459
|
+
const reasonCodes = new Set(compiled.renderModeHints.reasons.map((reason) => reason.code));
|
|
110460
|
+
if (reasonCodes.has("requestAnimationFrame")) {
|
|
110461
|
+
multiplier += 1;
|
|
110462
|
+
reasons.push("requestAnimationFrame");
|
|
110463
|
+
}
|
|
110464
|
+
if (reasonCodes.has("iframe")) {
|
|
110465
|
+
multiplier += 0.5;
|
|
110466
|
+
reasons.push("iframe");
|
|
110467
|
+
}
|
|
110468
|
+
if (composition.videos.length > 0) {
|
|
110469
|
+
multiplier += Math.min(2, composition.videos.length * 0.75);
|
|
110470
|
+
reasons.push(`${composition.videos.length} video${composition.videos.length === 1 ? "" : "s"}`);
|
|
110471
|
+
}
|
|
110472
|
+
if (composition.audios.length > 0) {
|
|
110473
|
+
multiplier += Math.min(1, composition.audios.length * 0.75);
|
|
110474
|
+
reasons.push(`${composition.audios.length} audio${composition.audios.length === 1 ? "" : "s"}`);
|
|
110475
|
+
}
|
|
110476
|
+
return {
|
|
110477
|
+
multiplier: Math.round(multiplier * 100) / 100,
|
|
110478
|
+
reasons
|
|
110479
|
+
};
|
|
110480
|
+
}
|
|
110481
|
+
function combineCaptureCostEstimates(staticCost, measuredCost) {
|
|
110482
|
+
if (!measuredCost || measuredCost.multiplier <= 1) return staticCost;
|
|
110483
|
+
if (staticCost.multiplier >= measuredCost.multiplier) {
|
|
110484
|
+
return {
|
|
110485
|
+
multiplier: staticCost.multiplier,
|
|
110486
|
+
reasons: [...staticCost.reasons, ...measuredCost.reasons],
|
|
110487
|
+
p95Ms: measuredCost.p95Ms
|
|
110488
|
+
};
|
|
110489
|
+
}
|
|
110490
|
+
return {
|
|
110491
|
+
multiplier: measuredCost.multiplier,
|
|
110492
|
+
reasons: [...measuredCost.reasons, ...staticCost.reasons],
|
|
110493
|
+
p95Ms: measuredCost.p95Ms
|
|
110494
|
+
};
|
|
110495
|
+
}
|
|
110496
|
+
var CAPTURE_CALIBRATION_TARGET_MS = 600;
|
|
110497
|
+
var MAX_MEASURED_CAPTURE_COST_MULTIPLIER = 8;
|
|
110498
|
+
var CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS = 3e4;
|
|
110499
|
+
function createCaptureCalibrationConfig(cfg) {
|
|
110500
|
+
return {
|
|
110501
|
+
...cfg,
|
|
110502
|
+
protocolTimeout: Math.min(cfg.protocolTimeout, CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS)
|
|
110503
|
+
};
|
|
110504
|
+
}
|
|
110505
|
+
function estimateMeasuredCaptureCostMultiplier(samples) {
|
|
110506
|
+
if (samples.length === 0) {
|
|
110507
|
+
return { multiplier: 1, reasons: [] };
|
|
110508
|
+
}
|
|
110509
|
+
const sorted = [...samples].sort((a, b) => a.captureTimeMs - b.captureTimeMs);
|
|
110510
|
+
const p95Index = Math.max(0, Math.ceil(sorted.length * 0.95) - 1);
|
|
110511
|
+
const p95Sample = sorted[p95Index] ?? sorted[sorted.length - 1];
|
|
110512
|
+
if (!p95Sample) {
|
|
110513
|
+
return { multiplier: 1, reasons: [] };
|
|
110514
|
+
}
|
|
110515
|
+
const p95Ms = Math.round(p95Sample.captureTimeMs);
|
|
110516
|
+
const multiplier = Math.min(
|
|
110517
|
+
MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
|
|
110518
|
+
Math.max(1, Math.round(p95Ms / CAPTURE_CALIBRATION_TARGET_MS * 100) / 100)
|
|
110519
|
+
);
|
|
110520
|
+
return {
|
|
110521
|
+
multiplier,
|
|
110522
|
+
reasons: multiplier > 1 ? [`calibration-p95=${p95Ms}ms`] : [],
|
|
110523
|
+
p95Ms
|
|
110524
|
+
};
|
|
110525
|
+
}
|
|
110526
|
+
function selectCaptureCalibrationFrames(totalFrames) {
|
|
110527
|
+
if (totalFrames <= 0) return [];
|
|
110528
|
+
const lastFrame = totalFrames - 1;
|
|
110529
|
+
const candidates = [
|
|
110530
|
+
0,
|
|
110531
|
+
Math.floor(totalFrames * 0.25),
|
|
110532
|
+
Math.floor(totalFrames * 0.5),
|
|
110533
|
+
Math.floor(totalFrames * 0.75),
|
|
110534
|
+
lastFrame
|
|
110535
|
+
];
|
|
110536
|
+
return Array.from(
|
|
110537
|
+
new Set(candidates.map((frame) => Math.max(0, Math.min(lastFrame, frame))))
|
|
110538
|
+
).sort((a, b) => a - b);
|
|
110539
|
+
}
|
|
110540
|
+
function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
110541
|
+
const ranges = [];
|
|
110542
|
+
let rangeStart = null;
|
|
110543
|
+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
110544
|
+
const framePath = join16(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
|
|
110545
|
+
const missing = !existsSync16(framePath);
|
|
110546
|
+
if (missing && rangeStart === null) {
|
|
110547
|
+
rangeStart = frameIndex;
|
|
110548
|
+
} else if (!missing && rangeStart !== null) {
|
|
110549
|
+
ranges.push({ startFrame: rangeStart, endFrame: frameIndex });
|
|
110550
|
+
rangeStart = null;
|
|
110551
|
+
}
|
|
110552
|
+
}
|
|
110553
|
+
if (rangeStart !== null) {
|
|
110554
|
+
ranges.push({ startFrame: rangeStart, endFrame: totalFrames });
|
|
110555
|
+
}
|
|
110556
|
+
return ranges;
|
|
110557
|
+
}
|
|
110558
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt) {
|
|
110559
|
+
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
110560
|
+
const batches = [];
|
|
110561
|
+
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
110562
|
+
const batchIndex = batches.length;
|
|
110563
|
+
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
110564
|
+
workerId,
|
|
110565
|
+
startFrame: range.startFrame,
|
|
110566
|
+
endFrame: range.endFrame,
|
|
110567
|
+
outputDir: join16(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`)
|
|
110568
|
+
}));
|
|
110569
|
+
batches.push(batch);
|
|
110570
|
+
}
|
|
110571
|
+
return batches;
|
|
110572
|
+
}
|
|
110573
|
+
function getNextRetryWorkerCount(currentWorkers) {
|
|
110574
|
+
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
110575
|
+
}
|
|
110576
|
+
function isRecoverableParallelCaptureError(error) {
|
|
110577
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110578
|
+
return message.includes("[Parallel] Capture failed") && /Runtime\.callFunctionOn timed out|HeadlessExperimental\.beginFrame timed out|Waiting failed|timeout exceeded|timed out|Navigation timeout|Protocol error|Target closed/i.test(
|
|
110579
|
+
message
|
|
110580
|
+
);
|
|
110581
|
+
}
|
|
110582
|
+
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
110583
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110584
|
+
return /HeadlessExperimental\.beginFrame timed out|beginFrame probe timeout|Another frame is pending|Frame still pending|Protocol error.*HeadlessExperimental\.beginFrame/i.test(
|
|
110585
|
+
message
|
|
110586
|
+
);
|
|
110587
|
+
}
|
|
110588
|
+
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
110589
|
+
let captured = 0;
|
|
110590
|
+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
110591
|
+
const framePath = join16(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
|
|
110592
|
+
if (existsSync16(framePath)) captured++;
|
|
110593
|
+
}
|
|
110594
|
+
return captured;
|
|
110595
|
+
}
|
|
110596
|
+
function countFrameRanges(ranges) {
|
|
110597
|
+
return ranges.reduce((sum, range) => sum + (range.endFrame - range.startFrame), 0);
|
|
110598
|
+
}
|
|
110599
|
+
async function measureCaptureCostFromSession(session, totalFrames, fps) {
|
|
110600
|
+
const sampledFrames = selectCaptureCalibrationFrames(totalFrames);
|
|
110601
|
+
const samples = [];
|
|
110602
|
+
for (const frameIndex of sampledFrames) {
|
|
110603
|
+
const time = frameIndex / fps;
|
|
110604
|
+
const startedAt = Date.now();
|
|
110605
|
+
const result = await captureFrameToBuffer(session, frameIndex, time);
|
|
110606
|
+
samples.push({
|
|
110607
|
+
frameIndex,
|
|
110608
|
+
captureTimeMs: result.captureTimeMs || Date.now() - startedAt
|
|
110609
|
+
});
|
|
110610
|
+
}
|
|
110611
|
+
return {
|
|
110612
|
+
estimate: estimateMeasuredCaptureCostMultiplier(samples),
|
|
110613
|
+
samples
|
|
110614
|
+
};
|
|
110615
|
+
}
|
|
110616
|
+
async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
110617
|
+
const attempts = [];
|
|
110618
|
+
let currentWorkers = options.initialWorkerCount;
|
|
110619
|
+
let missingRanges = null;
|
|
110620
|
+
let attempt = 0;
|
|
110621
|
+
while (true) {
|
|
110622
|
+
const frameCount = missingRanges ? countFrameRanges(missingRanges) : options.totalFrames;
|
|
110623
|
+
attempts.push({
|
|
110624
|
+
attempt,
|
|
110625
|
+
workers: currentWorkers,
|
|
110626
|
+
frameCount,
|
|
110627
|
+
reason: attempt === 0 ? "initial" : "retry"
|
|
110628
|
+
});
|
|
110629
|
+
const attemptWorkDir = join16(options.workDir, `capture-attempt-${attempt}`);
|
|
110630
|
+
const batches = missingRanges ? buildMissingFrameRetryBatches(missingRanges, currentWorkers, attemptWorkDir, attempt) : [distributeFrames(options.totalFrames, currentWorkers, attemptWorkDir)];
|
|
110631
|
+
try {
|
|
110632
|
+
for (const tasks of batches) {
|
|
110633
|
+
const capturedBeforeBatch = countCapturedFrames(
|
|
110634
|
+
options.totalFrames,
|
|
110635
|
+
options.framesDir,
|
|
110636
|
+
options.frameExt
|
|
110637
|
+
);
|
|
110638
|
+
try {
|
|
110639
|
+
await executeParallelCapture(
|
|
110640
|
+
options.serverUrl,
|
|
110641
|
+
attemptWorkDir,
|
|
110642
|
+
tasks,
|
|
110643
|
+
options.captureOptions,
|
|
110644
|
+
options.createBeforeCaptureHook,
|
|
110645
|
+
options.abortSignal,
|
|
110646
|
+
options.onProgress ? (progress) => {
|
|
110647
|
+
options.onProgress?.({
|
|
110648
|
+
...progress,
|
|
110649
|
+
totalFrames: options.totalFrames,
|
|
110650
|
+
capturedFrames: Math.min(
|
|
110651
|
+
options.totalFrames,
|
|
110652
|
+
capturedBeforeBatch + progress.capturedFrames
|
|
110653
|
+
)
|
|
110654
|
+
});
|
|
110655
|
+
} : void 0,
|
|
110656
|
+
void 0,
|
|
110657
|
+
options.cfg
|
|
110658
|
+
);
|
|
110659
|
+
} finally {
|
|
110660
|
+
await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
|
|
110661
|
+
}
|
|
110662
|
+
}
|
|
110663
|
+
const remaining = findMissingFrameRanges(
|
|
110664
|
+
options.totalFrames,
|
|
110665
|
+
options.framesDir,
|
|
110666
|
+
options.frameExt
|
|
110667
|
+
);
|
|
110668
|
+
if (remaining.length === 0) {
|
|
110669
|
+
return attempts;
|
|
110670
|
+
}
|
|
110671
|
+
if (!options.allowRetry || currentWorkers <= 1) {
|
|
110672
|
+
throw new Error(
|
|
110673
|
+
`[Render] Capture completed but ${countFrameRanges(remaining)} frame(s) are missing`
|
|
110674
|
+
);
|
|
110675
|
+
}
|
|
110676
|
+
const nextWorkers = getNextRetryWorkerCount(currentWorkers);
|
|
110677
|
+
options.log.warn("[Render] Retrying missing captured frames with fewer workers.", {
|
|
110678
|
+
fromWorkers: currentWorkers,
|
|
110679
|
+
toWorkers: nextWorkers,
|
|
110680
|
+
missingFrames: countFrameRanges(remaining)
|
|
110681
|
+
});
|
|
110682
|
+
currentWorkers = nextWorkers;
|
|
110683
|
+
missingRanges = remaining;
|
|
110684
|
+
attempt++;
|
|
110685
|
+
} catch (error) {
|
|
110686
|
+
const remaining = findMissingFrameRanges(
|
|
110687
|
+
options.totalFrames,
|
|
110688
|
+
options.framesDir,
|
|
110689
|
+
options.frameExt
|
|
110690
|
+
);
|
|
110691
|
+
if (remaining.length === 0) {
|
|
110692
|
+
return attempts;
|
|
110693
|
+
}
|
|
110694
|
+
if (!options.allowRetry || currentWorkers <= 1 || !isRecoverableParallelCaptureError(error)) {
|
|
110695
|
+
throw error;
|
|
110696
|
+
}
|
|
110697
|
+
const nextWorkers = getNextRetryWorkerCount(currentWorkers);
|
|
110698
|
+
options.log.warn("[Render] Parallel capture timed out; retrying missing frames.", {
|
|
110699
|
+
fromWorkers: currentWorkers,
|
|
110700
|
+
toWorkers: nextWorkers,
|
|
110701
|
+
missingFrames: countFrameRanges(remaining),
|
|
110702
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110703
|
+
});
|
|
110704
|
+
currentWorkers = nextWorkers;
|
|
110705
|
+
missingRanges = remaining;
|
|
110706
|
+
attempt++;
|
|
110707
|
+
}
|
|
110708
|
+
}
|
|
110709
|
+
}
|
|
110403
110710
|
function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer) {
|
|
110404
110711
|
const frameDir = hdrFrameDirs.get(el.id);
|
|
110405
110712
|
const startTime = hdrStartTimes.get(el.id);
|
|
@@ -111026,7 +111333,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111026
111333
|
let extractionResult = null;
|
|
111027
111334
|
const nativeHdrVideoIds = /* @__PURE__ */ new Set();
|
|
111028
111335
|
const videoTransfers = /* @__PURE__ */ new Map();
|
|
111029
|
-
if (job.config.
|
|
111336
|
+
if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
|
|
111030
111337
|
await Promise.all(
|
|
111031
111338
|
composition.videos.map(async (v) => {
|
|
111032
111339
|
let videoPath = v.src;
|
|
@@ -111047,7 +111354,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111047
111354
|
const imageTransfers = /* @__PURE__ */ new Map();
|
|
111048
111355
|
const hdrImageSrcPaths = /* @__PURE__ */ new Map();
|
|
111049
111356
|
const imageColorSpaces = [];
|
|
111050
|
-
if (job.config.
|
|
111357
|
+
if (job.config.hdrMode !== "force-sdr" && composition.images.length > 0) {
|
|
111051
111358
|
const probed = await Promise.all(
|
|
111052
111359
|
composition.images.map(async (img) => {
|
|
111053
111360
|
let imgPath = img.src;
|
|
@@ -111104,28 +111411,53 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111104
111411
|
perfStages.videoExtractMs = Date.now() - stage2Start;
|
|
111105
111412
|
}
|
|
111106
111413
|
let effectiveHdr;
|
|
111107
|
-
|
|
111414
|
+
let forcedHdrWithoutSources = false;
|
|
111415
|
+
{
|
|
111416
|
+
const hdrMode = job.config.hdrMode ?? "auto";
|
|
111108
111417
|
const videoColorSpaces = (extractionResult?.extracted ?? []).map(
|
|
111109
111418
|
(ext) => ext.metadata.colorSpace
|
|
111110
111419
|
);
|
|
111111
111420
|
const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
|
|
111112
|
-
|
|
111113
|
-
|
|
111114
|
-
|
|
111421
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
111422
|
+
if (hdrMode === "force-sdr") {
|
|
111423
|
+
effectiveHdr = void 0;
|
|
111424
|
+
} else if (hdrMode === "force-hdr") {
|
|
111425
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
111426
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
111427
|
+
} else {
|
|
111428
|
+
effectiveHdr = { transfer: "hlg" };
|
|
111429
|
+
forcedHdrWithoutSources = true;
|
|
111430
|
+
}
|
|
111431
|
+
} else {
|
|
111432
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
111115
111433
|
effectiveHdr = { transfer: info.dominantTransfer };
|
|
111116
111434
|
}
|
|
111117
111435
|
}
|
|
111118
111436
|
}
|
|
111119
111437
|
if (effectiveHdr && outputFormat !== "mp4") {
|
|
111438
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
111120
111439
|
log.warn(
|
|
111121
|
-
`[Render]
|
|
111440
|
+
`[Render] ${hdrSourceReason}, but format is "${outputFormat}" \u2014 falling back to SDR. HDR + alpha is not supported. Use --format mp4 for HDR10 output.`
|
|
111122
111441
|
);
|
|
111123
111442
|
effectiveHdr = void 0;
|
|
111124
111443
|
}
|
|
111125
|
-
|
|
111126
|
-
|
|
111127
|
-
|
|
111128
|
-
|
|
111444
|
+
{
|
|
111445
|
+
const hdrMode = job.config.hdrMode ?? "auto";
|
|
111446
|
+
if (forcedHdrWithoutSources) {
|
|
111447
|
+
log.warn(
|
|
111448
|
+
"[Render] HDR forced by --hdr flag, but no HDR sources were detected \u2014 defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays."
|
|
111449
|
+
);
|
|
111450
|
+
}
|
|
111451
|
+
if (effectiveHdr) {
|
|
111452
|
+
const reason = hdrMode === "force-hdr" ? forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected \u2014 defaulting to HLG)" : "forced by --hdr flag" : "auto-detected from source(s)";
|
|
111453
|
+
log.info(
|
|
111454
|
+
`[Render] HDR ${reason} \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
111455
|
+
);
|
|
111456
|
+
} else if (hdrMode === "force-sdr") {
|
|
111457
|
+
log.info("[Render] SDR forced by --sdr flag");
|
|
111458
|
+
} else {
|
|
111459
|
+
log.info("[Render] No HDR sources detected \u2014 rendering SDR");
|
|
111460
|
+
}
|
|
111129
111461
|
}
|
|
111130
111462
|
const stage3Start = Date.now();
|
|
111131
111463
|
updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
|
|
@@ -111172,7 +111504,101 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111172
111504
|
...captureOptions,
|
|
111173
111505
|
skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
|
|
111174
111506
|
});
|
|
111175
|
-
|
|
111507
|
+
let captureCalibration;
|
|
111508
|
+
let switchedToScreenshotAfterCalibration = false;
|
|
111509
|
+
if (job.config.workers === void 0 && totalFrames >= 60) {
|
|
111510
|
+
const calibrationDir = join16(workDir, "capture-calibration");
|
|
111511
|
+
const calibrationCfg = createCaptureCalibrationConfig(cfg);
|
|
111512
|
+
const videoInjector = createVideoFrameInjector(frameLookup);
|
|
111513
|
+
let calibrationSession = null;
|
|
111514
|
+
try {
|
|
111515
|
+
calibrationSession = await createCaptureSession(
|
|
111516
|
+
fileServer.url,
|
|
111517
|
+
calibrationDir,
|
|
111518
|
+
buildHdrCaptureOptions(),
|
|
111519
|
+
videoInjector,
|
|
111520
|
+
calibrationCfg
|
|
111521
|
+
);
|
|
111522
|
+
if (!calibrationSession.isInitialized) {
|
|
111523
|
+
await initializeSession(calibrationSession);
|
|
111524
|
+
}
|
|
111525
|
+
assertNotAborted();
|
|
111526
|
+
captureCalibration = await measureCaptureCostFromSession(
|
|
111527
|
+
calibrationSession,
|
|
111528
|
+
totalFrames,
|
|
111529
|
+
job.config.fps
|
|
111530
|
+
);
|
|
111531
|
+
if (captureCalibration.estimate.multiplier > 1) {
|
|
111532
|
+
log.warn("[Render] Measured slow frame capture during auto-worker calibration.", {
|
|
111533
|
+
multiplier: captureCalibration.estimate.multiplier,
|
|
111534
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
111535
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
|
|
111536
|
+
});
|
|
111537
|
+
} else {
|
|
111538
|
+
log.debug("[Render] Auto-worker calibration kept baseline capture cost.", {
|
|
111539
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
111540
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
|
|
111541
|
+
});
|
|
111542
|
+
}
|
|
111543
|
+
} catch (error) {
|
|
111544
|
+
const shouldFallbackToScreenshot = !cfg.forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
111545
|
+
if (shouldFallbackToScreenshot) {
|
|
111546
|
+
cfg.forceScreenshot = true;
|
|
111547
|
+
switchedToScreenshotAfterCalibration = true;
|
|
111548
|
+
if (probeSession) {
|
|
111549
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111550
|
+
await closeCaptureSession(probeSession).catch(() => {
|
|
111551
|
+
});
|
|
111552
|
+
probeSession = null;
|
|
111553
|
+
}
|
|
111554
|
+
}
|
|
111555
|
+
captureCalibration = {
|
|
111556
|
+
estimate: {
|
|
111557
|
+
multiplier: MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
|
|
111558
|
+
reasons: shouldFallbackToScreenshot ? ["calibration-beginframe-timeout", "screenshot-fallback"] : ["calibration-failed"]
|
|
111559
|
+
},
|
|
111560
|
+
samples: []
|
|
111561
|
+
};
|
|
111562
|
+
if (shouldFallbackToScreenshot) {
|
|
111563
|
+
log.warn(
|
|
111564
|
+
"[Render] BeginFrame auto-worker calibration timed out; falling back to screenshot capture mode.",
|
|
111565
|
+
{
|
|
111566
|
+
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
111567
|
+
error: error instanceof Error ? error.message : String(error)
|
|
111568
|
+
}
|
|
111569
|
+
);
|
|
111570
|
+
} else {
|
|
111571
|
+
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
111572
|
+
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
111573
|
+
error: error instanceof Error ? error.message : String(error)
|
|
111574
|
+
});
|
|
111575
|
+
}
|
|
111576
|
+
} finally {
|
|
111577
|
+
if (calibrationSession) {
|
|
111578
|
+
lastBrowserConsole = calibrationSession.browserConsoleBuffer;
|
|
111579
|
+
await closeCaptureSession(calibrationSession).catch(() => {
|
|
111580
|
+
});
|
|
111581
|
+
}
|
|
111582
|
+
}
|
|
111583
|
+
}
|
|
111584
|
+
let workerCount = resolveRenderWorkerCount(
|
|
111585
|
+
totalFrames,
|
|
111586
|
+
job.config.workers,
|
|
111587
|
+
cfg,
|
|
111588
|
+
compiled,
|
|
111589
|
+
composition,
|
|
111590
|
+
log,
|
|
111591
|
+
captureCalibration?.estimate
|
|
111592
|
+
);
|
|
111593
|
+
if (switchedToScreenshotAfterCalibration && workerCount > 1) {
|
|
111594
|
+
workerCount = 1;
|
|
111595
|
+
}
|
|
111596
|
+
if (workerCount > 1 && probeSession) {
|
|
111597
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111598
|
+
await closeCaptureSession(probeSession);
|
|
111599
|
+
probeSession = null;
|
|
111600
|
+
}
|
|
111601
|
+
const captureAttempts = [];
|
|
111176
111602
|
const FORMAT_EXT = {
|
|
111177
111603
|
mp4: ".mp4",
|
|
111178
111604
|
webm: ".webm",
|
|
@@ -111764,15 +112190,18 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111764
112190
|
perfStages.encodeMs = encodeResult.durationMs;
|
|
111765
112191
|
} else {
|
|
111766
112192
|
if (workerCount > 1) {
|
|
111767
|
-
const
|
|
111768
|
-
|
|
111769
|
-
fileServer.url,
|
|
112193
|
+
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
112194
|
+
serverUrl: fileServer.url,
|
|
111770
112195
|
workDir,
|
|
111771
|
-
|
|
111772
|
-
|
|
111773
|
-
|
|
112196
|
+
framesDir,
|
|
112197
|
+
totalFrames: job.totalFrames,
|
|
112198
|
+
initialWorkerCount: workerCount,
|
|
112199
|
+
allowRetry: job.config.workers === void 0,
|
|
112200
|
+
frameExt: needsAlpha ? "png" : "jpg",
|
|
112201
|
+
captureOptions: buildHdrCaptureOptions(),
|
|
112202
|
+
createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
|
|
111774
112203
|
abortSignal,
|
|
111775
|
-
(progress) => {
|
|
112204
|
+
onProgress: (progress) => {
|
|
111776
112205
|
job.framesRendered = progress.capturedFrames;
|
|
111777
112206
|
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
111778
112207
|
const progressPct = 25 + frameProgress * 45;
|
|
@@ -111780,16 +112209,20 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111780
112209
|
updateJobStatus(
|
|
111781
112210
|
job,
|
|
111782
112211
|
"rendering",
|
|
111783
|
-
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${
|
|
112212
|
+
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
111784
112213
|
Math.round(progressPct),
|
|
111785
112214
|
onProgress
|
|
111786
112215
|
);
|
|
111787
112216
|
}
|
|
111788
112217
|
},
|
|
111789
|
-
|
|
111790
|
-
|
|
111791
|
-
);
|
|
111792
|
-
|
|
112218
|
+
cfg,
|
|
112219
|
+
log
|
|
112220
|
+
});
|
|
112221
|
+
captureAttempts.push(...attempts);
|
|
112222
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
112223
|
+
if (lastAttempt) {
|
|
112224
|
+
workerCount = lastAttempt.workers;
|
|
112225
|
+
}
|
|
111793
112226
|
if (probeSession) {
|
|
111794
112227
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111795
112228
|
await closeCaptureSession(probeSession);
|
|
@@ -111959,6 +112392,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111959
112392
|
stages: perfStages,
|
|
111960
112393
|
videoExtractBreakdown: extractionResult?.phaseBreakdown,
|
|
111961
112394
|
tmpPeakBytes,
|
|
112395
|
+
captureCalibration: captureCalibration ? {
|
|
112396
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex),
|
|
112397
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
112398
|
+
multiplier: captureCalibration.estimate.multiplier,
|
|
112399
|
+
reasons: captureCalibration.estimate.reasons
|
|
112400
|
+
} : void 0,
|
|
112401
|
+
captureAttempts: captureAttempts.length > 0 ? captureAttempts : void 0,
|
|
111962
112402
|
hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
|
|
111963
112403
|
captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0,
|
|
111964
112404
|
peakRssMb: Math.round(peakRssBytes / (1024 * 1024)),
|