@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/public-server.js
CHANGED
|
@@ -103358,7 +103358,7 @@ var captionRules = [
|
|
|
103358
103358
|
const hasHardKill = /\.set\s*\([^,]+,\s*\{[^}]*(?:visibility\s*:\s*["']hidden["']|opacity\s*:\s*0)/.test(
|
|
103359
103359
|
content
|
|
103360
103360
|
);
|
|
103361
|
-
const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /
|
|
103361
|
+
const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /karaoke|caption[-_]?(?:group|word|line|block)|cg-/.test(content);
|
|
103362
103362
|
if (hasCaptionLoop && hasExitTween && !hasHardKill) {
|
|
103363
103363
|
findings.push({
|
|
103364
103364
|
code: "caption_exit_missing_hard_kill",
|
|
@@ -107053,6 +107053,7 @@ function calculateOptimalWorkers(totalFrames, requested, config2) {
|
|
|
107053
107053
|
const effectiveCoresPerWorker = config2?.coresPerWorker ?? DEFAULT_CONFIG.coresPerWorker;
|
|
107054
107054
|
const effectiveMinParallelFrames = config2?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
|
|
107055
107055
|
const effectiveLargeRenderThreshold = config2?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
|
|
107056
|
+
const captureCostMultiplier = Math.max(1, config2?.captureCostMultiplier ?? 1);
|
|
107056
107057
|
if (requested !== void 0) {
|
|
107057
107058
|
return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
|
|
107058
107059
|
}
|
|
@@ -107065,8 +107066,14 @@ function calculateOptimalWorkers(totalFrames, requested, config2) {
|
|
|
107065
107066
|
const optimal = Math.min(cpuBasedWorkers, memoryBasedWorkers, frameBasedWorkers);
|
|
107066
107067
|
const minWorkersForJob = totalFrames >= effectiveMinParallelFrames ? 2 : MIN_WORKERS;
|
|
107067
107068
|
let finalWorkers = Math.max(minWorkersForJob, Math.min(effectiveMaxWorkers, optimal));
|
|
107068
|
-
|
|
107069
|
-
|
|
107069
|
+
const weightedFrames = totalFrames * captureCostMultiplier;
|
|
107070
|
+
const contentionThreshold = Math.max(
|
|
107071
|
+
effectiveMinParallelFrames,
|
|
107072
|
+
Math.floor(effectiveLargeRenderThreshold / 3)
|
|
107073
|
+
);
|
|
107074
|
+
if (totalFrames >= effectiveLargeRenderThreshold || weightedFrames >= contentionThreshold) {
|
|
107075
|
+
const weightedCoresPerWorker = effectiveCoresPerWorker * captureCostMultiplier;
|
|
107076
|
+
const cpuScaledMax = Math.max(MIN_WORKERS, Math.floor(cpuCount / weightedCoresPerWorker));
|
|
107070
107077
|
if (finalWorkers > cpuScaledMax) {
|
|
107071
107078
|
finalWorkers = cpuScaledMax;
|
|
107072
107079
|
}
|
|
@@ -109588,6 +109595,18 @@ function detectRenderModeHints(html) {
|
|
|
109588
109595
|
reasons
|
|
109589
109596
|
};
|
|
109590
109597
|
}
|
|
109598
|
+
var SHADER_TRANSITION_USAGE_PATTERN = /\b(?:(?:window|globalThis)\s*\.\s*)?HyperShader\s*\.\s*init\s*\(|\b__hf\s*\.\s*transitions\s*=/;
|
|
109599
|
+
function detectShaderTransitionUsage(html) {
|
|
109600
|
+
let scriptMatch;
|
|
109601
|
+
const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
|
|
109602
|
+
while ((scriptMatch = scriptPattern.exec(html)) !== null) {
|
|
109603
|
+
const attrs = scriptMatch[1] || "";
|
|
109604
|
+
if (/\bsrc\s*=/i.test(attrs)) continue;
|
|
109605
|
+
const content = stripJsComments(stripCompilerMountBootstrap(scriptMatch[2] || ""));
|
|
109606
|
+
if (SHADER_TRANSITION_USAGE_PATTERN.test(content)) return true;
|
|
109607
|
+
}
|
|
109608
|
+
return false;
|
|
109609
|
+
}
|
|
109591
109610
|
async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
|
|
109592
109611
|
let filePath = src;
|
|
109593
109612
|
if (isHttpUrl(src)) {
|
|
@@ -110124,6 +110143,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
110124
110143
|
"$1"
|
|
110125
110144
|
);
|
|
110126
110145
|
const renderModeHints = detectRenderModeHints(sanitizedHtml);
|
|
110146
|
+
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
110127
110147
|
const coalescedHtml = await injectDeterministicFontFaces(
|
|
110128
110148
|
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
|
|
110129
110149
|
);
|
|
@@ -110171,7 +110191,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
110171
110191
|
width,
|
|
110172
110192
|
height,
|
|
110173
110193
|
staticDuration,
|
|
110174
|
-
renderModeHints
|
|
110194
|
+
renderModeHints,
|
|
110195
|
+
hasShaderTransitions
|
|
110175
110196
|
};
|
|
110176
110197
|
}
|
|
110177
110198
|
async function discoverMediaFromBrowser(page) {
|
|
@@ -110273,7 +110294,8 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
|
|
|
110273
110294
|
audios,
|
|
110274
110295
|
images,
|
|
110275
110296
|
unresolvedCompositions: remaining,
|
|
110276
|
-
renderModeHints: compiled.renderModeHints
|
|
110297
|
+
renderModeHints: compiled.renderModeHints,
|
|
110298
|
+
hasShaderTransitions: compiled.hasShaderTransitions
|
|
110277
110299
|
};
|
|
110278
110300
|
}
|
|
110279
110301
|
|
|
@@ -110552,7 +110574,8 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
|
|
|
110552
110574
|
mediaStart: a.mediaStart
|
|
110553
110575
|
})),
|
|
110554
110576
|
subCompositions: Array.from(compiled.subCompositions.keys()),
|
|
110555
|
-
renderModeHints: compiled.renderModeHints
|
|
110577
|
+
renderModeHints: compiled.renderModeHints,
|
|
110578
|
+
hasShaderTransitions: compiled.hasShaderTransitions
|
|
110556
110579
|
};
|
|
110557
110580
|
writeFileSync5(join16(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
|
|
110558
110581
|
}
|
|
@@ -110565,6 +110588,290 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
|
|
|
110565
110588
|
reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
|
|
110566
110589
|
});
|
|
110567
110590
|
}
|
|
110591
|
+
function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log = defaultLogger, measuredCaptureCost) {
|
|
110592
|
+
const captureCost = combineCaptureCostEstimates(
|
|
110593
|
+
estimateCaptureCostMultiplier(compiled, composition),
|
|
110594
|
+
measuredCaptureCost
|
|
110595
|
+
);
|
|
110596
|
+
const workerCount = calculateOptimalWorkers(totalFrames, requestedWorkers, {
|
|
110597
|
+
...cfg,
|
|
110598
|
+
captureCostMultiplier: captureCost.multiplier
|
|
110599
|
+
});
|
|
110600
|
+
if (requestedWorkers !== void 0 || captureCost.multiplier <= 1) {
|
|
110601
|
+
return workerCount;
|
|
110602
|
+
}
|
|
110603
|
+
const baselineWorkers = calculateOptimalWorkers(totalFrames, void 0, cfg);
|
|
110604
|
+
if (workerCount < baselineWorkers) {
|
|
110605
|
+
log.warn(
|
|
110606
|
+
"[Render] Reduced auto worker count for high-cost capture workload to avoid Chrome compositor starvation.",
|
|
110607
|
+
{
|
|
110608
|
+
from: baselineWorkers,
|
|
110609
|
+
to: workerCount,
|
|
110610
|
+
costMultiplier: captureCost.multiplier,
|
|
110611
|
+
reasons: captureCost.reasons
|
|
110612
|
+
}
|
|
110613
|
+
);
|
|
110614
|
+
}
|
|
110615
|
+
return workerCount;
|
|
110616
|
+
}
|
|
110617
|
+
function estimateCaptureCostMultiplier(compiled, composition) {
|
|
110618
|
+
let multiplier = 1;
|
|
110619
|
+
const reasons = [];
|
|
110620
|
+
if (compiled.hasShaderTransitions) {
|
|
110621
|
+
multiplier += 2;
|
|
110622
|
+
reasons.push("shader-transitions");
|
|
110623
|
+
}
|
|
110624
|
+
const reasonCodes = new Set(compiled.renderModeHints.reasons.map((reason) => reason.code));
|
|
110625
|
+
if (reasonCodes.has("requestAnimationFrame")) {
|
|
110626
|
+
multiplier += 1;
|
|
110627
|
+
reasons.push("requestAnimationFrame");
|
|
110628
|
+
}
|
|
110629
|
+
if (reasonCodes.has("iframe")) {
|
|
110630
|
+
multiplier += 0.5;
|
|
110631
|
+
reasons.push("iframe");
|
|
110632
|
+
}
|
|
110633
|
+
if (composition.videos.length > 0) {
|
|
110634
|
+
multiplier += Math.min(2, composition.videos.length * 0.75);
|
|
110635
|
+
reasons.push(`${composition.videos.length} video${composition.videos.length === 1 ? "" : "s"}`);
|
|
110636
|
+
}
|
|
110637
|
+
if (composition.audios.length > 0) {
|
|
110638
|
+
multiplier += Math.min(1, composition.audios.length * 0.75);
|
|
110639
|
+
reasons.push(`${composition.audios.length} audio${composition.audios.length === 1 ? "" : "s"}`);
|
|
110640
|
+
}
|
|
110641
|
+
return {
|
|
110642
|
+
multiplier: Math.round(multiplier * 100) / 100,
|
|
110643
|
+
reasons
|
|
110644
|
+
};
|
|
110645
|
+
}
|
|
110646
|
+
function combineCaptureCostEstimates(staticCost, measuredCost) {
|
|
110647
|
+
if (!measuredCost || measuredCost.multiplier <= 1) return staticCost;
|
|
110648
|
+
if (staticCost.multiplier >= measuredCost.multiplier) {
|
|
110649
|
+
return {
|
|
110650
|
+
multiplier: staticCost.multiplier,
|
|
110651
|
+
reasons: [...staticCost.reasons, ...measuredCost.reasons],
|
|
110652
|
+
p95Ms: measuredCost.p95Ms
|
|
110653
|
+
};
|
|
110654
|
+
}
|
|
110655
|
+
return {
|
|
110656
|
+
multiplier: measuredCost.multiplier,
|
|
110657
|
+
reasons: [...measuredCost.reasons, ...staticCost.reasons],
|
|
110658
|
+
p95Ms: measuredCost.p95Ms
|
|
110659
|
+
};
|
|
110660
|
+
}
|
|
110661
|
+
var CAPTURE_CALIBRATION_TARGET_MS = 600;
|
|
110662
|
+
var MAX_MEASURED_CAPTURE_COST_MULTIPLIER = 8;
|
|
110663
|
+
var CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS = 3e4;
|
|
110664
|
+
function createCaptureCalibrationConfig(cfg) {
|
|
110665
|
+
return {
|
|
110666
|
+
...cfg,
|
|
110667
|
+
protocolTimeout: Math.min(cfg.protocolTimeout, CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS)
|
|
110668
|
+
};
|
|
110669
|
+
}
|
|
110670
|
+
function estimateMeasuredCaptureCostMultiplier(samples) {
|
|
110671
|
+
if (samples.length === 0) {
|
|
110672
|
+
return { multiplier: 1, reasons: [] };
|
|
110673
|
+
}
|
|
110674
|
+
const sorted = [...samples].sort((a, b) => a.captureTimeMs - b.captureTimeMs);
|
|
110675
|
+
const p95Index = Math.max(0, Math.ceil(sorted.length * 0.95) - 1);
|
|
110676
|
+
const p95Sample = sorted[p95Index] ?? sorted[sorted.length - 1];
|
|
110677
|
+
if (!p95Sample) {
|
|
110678
|
+
return { multiplier: 1, reasons: [] };
|
|
110679
|
+
}
|
|
110680
|
+
const p95Ms = Math.round(p95Sample.captureTimeMs);
|
|
110681
|
+
const multiplier = Math.min(
|
|
110682
|
+
MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
|
|
110683
|
+
Math.max(1, Math.round(p95Ms / CAPTURE_CALIBRATION_TARGET_MS * 100) / 100)
|
|
110684
|
+
);
|
|
110685
|
+
return {
|
|
110686
|
+
multiplier,
|
|
110687
|
+
reasons: multiplier > 1 ? [`calibration-p95=${p95Ms}ms`] : [],
|
|
110688
|
+
p95Ms
|
|
110689
|
+
};
|
|
110690
|
+
}
|
|
110691
|
+
function selectCaptureCalibrationFrames(totalFrames) {
|
|
110692
|
+
if (totalFrames <= 0) return [];
|
|
110693
|
+
const lastFrame = totalFrames - 1;
|
|
110694
|
+
const candidates = [
|
|
110695
|
+
0,
|
|
110696
|
+
Math.floor(totalFrames * 0.25),
|
|
110697
|
+
Math.floor(totalFrames * 0.5),
|
|
110698
|
+
Math.floor(totalFrames * 0.75),
|
|
110699
|
+
lastFrame
|
|
110700
|
+
];
|
|
110701
|
+
return Array.from(
|
|
110702
|
+
new Set(candidates.map((frame) => Math.max(0, Math.min(lastFrame, frame))))
|
|
110703
|
+
).sort((a, b) => a - b);
|
|
110704
|
+
}
|
|
110705
|
+
function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
110706
|
+
const ranges = [];
|
|
110707
|
+
let rangeStart = null;
|
|
110708
|
+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
110709
|
+
const framePath = join16(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
|
|
110710
|
+
const missing = !existsSync16(framePath);
|
|
110711
|
+
if (missing && rangeStart === null) {
|
|
110712
|
+
rangeStart = frameIndex;
|
|
110713
|
+
} else if (!missing && rangeStart !== null) {
|
|
110714
|
+
ranges.push({ startFrame: rangeStart, endFrame: frameIndex });
|
|
110715
|
+
rangeStart = null;
|
|
110716
|
+
}
|
|
110717
|
+
}
|
|
110718
|
+
if (rangeStart !== null) {
|
|
110719
|
+
ranges.push({ startFrame: rangeStart, endFrame: totalFrames });
|
|
110720
|
+
}
|
|
110721
|
+
return ranges;
|
|
110722
|
+
}
|
|
110723
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt) {
|
|
110724
|
+
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
110725
|
+
const batches = [];
|
|
110726
|
+
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
110727
|
+
const batchIndex = batches.length;
|
|
110728
|
+
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
110729
|
+
workerId,
|
|
110730
|
+
startFrame: range.startFrame,
|
|
110731
|
+
endFrame: range.endFrame,
|
|
110732
|
+
outputDir: join16(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`)
|
|
110733
|
+
}));
|
|
110734
|
+
batches.push(batch);
|
|
110735
|
+
}
|
|
110736
|
+
return batches;
|
|
110737
|
+
}
|
|
110738
|
+
function getNextRetryWorkerCount(currentWorkers) {
|
|
110739
|
+
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
110740
|
+
}
|
|
110741
|
+
function isRecoverableParallelCaptureError(error) {
|
|
110742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110743
|
+
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(
|
|
110744
|
+
message
|
|
110745
|
+
);
|
|
110746
|
+
}
|
|
110747
|
+
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
110748
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110749
|
+
return /HeadlessExperimental\.beginFrame timed out|beginFrame probe timeout|Another frame is pending|Frame still pending|Protocol error.*HeadlessExperimental\.beginFrame/i.test(
|
|
110750
|
+
message
|
|
110751
|
+
);
|
|
110752
|
+
}
|
|
110753
|
+
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
110754
|
+
let captured = 0;
|
|
110755
|
+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
110756
|
+
const framePath = join16(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
|
|
110757
|
+
if (existsSync16(framePath)) captured++;
|
|
110758
|
+
}
|
|
110759
|
+
return captured;
|
|
110760
|
+
}
|
|
110761
|
+
function countFrameRanges(ranges) {
|
|
110762
|
+
return ranges.reduce((sum, range) => sum + (range.endFrame - range.startFrame), 0);
|
|
110763
|
+
}
|
|
110764
|
+
async function measureCaptureCostFromSession(session, totalFrames, fps) {
|
|
110765
|
+
const sampledFrames = selectCaptureCalibrationFrames(totalFrames);
|
|
110766
|
+
const samples = [];
|
|
110767
|
+
for (const frameIndex of sampledFrames) {
|
|
110768
|
+
const time = frameIndex / fps;
|
|
110769
|
+
const startedAt = Date.now();
|
|
110770
|
+
const result = await captureFrameToBuffer(session, frameIndex, time);
|
|
110771
|
+
samples.push({
|
|
110772
|
+
frameIndex,
|
|
110773
|
+
captureTimeMs: result.captureTimeMs || Date.now() - startedAt
|
|
110774
|
+
});
|
|
110775
|
+
}
|
|
110776
|
+
return {
|
|
110777
|
+
estimate: estimateMeasuredCaptureCostMultiplier(samples),
|
|
110778
|
+
samples
|
|
110779
|
+
};
|
|
110780
|
+
}
|
|
110781
|
+
async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
110782
|
+
const attempts = [];
|
|
110783
|
+
let currentWorkers = options.initialWorkerCount;
|
|
110784
|
+
let missingRanges = null;
|
|
110785
|
+
let attempt = 0;
|
|
110786
|
+
while (true) {
|
|
110787
|
+
const frameCount = missingRanges ? countFrameRanges(missingRanges) : options.totalFrames;
|
|
110788
|
+
attempts.push({
|
|
110789
|
+
attempt,
|
|
110790
|
+
workers: currentWorkers,
|
|
110791
|
+
frameCount,
|
|
110792
|
+
reason: attempt === 0 ? "initial" : "retry"
|
|
110793
|
+
});
|
|
110794
|
+
const attemptWorkDir = join16(options.workDir, `capture-attempt-${attempt}`);
|
|
110795
|
+
const batches = missingRanges ? buildMissingFrameRetryBatches(missingRanges, currentWorkers, attemptWorkDir, attempt) : [distributeFrames(options.totalFrames, currentWorkers, attemptWorkDir)];
|
|
110796
|
+
try {
|
|
110797
|
+
for (const tasks of batches) {
|
|
110798
|
+
const capturedBeforeBatch = countCapturedFrames(
|
|
110799
|
+
options.totalFrames,
|
|
110800
|
+
options.framesDir,
|
|
110801
|
+
options.frameExt
|
|
110802
|
+
);
|
|
110803
|
+
try {
|
|
110804
|
+
await executeParallelCapture(
|
|
110805
|
+
options.serverUrl,
|
|
110806
|
+
attemptWorkDir,
|
|
110807
|
+
tasks,
|
|
110808
|
+
options.captureOptions,
|
|
110809
|
+
options.createBeforeCaptureHook,
|
|
110810
|
+
options.abortSignal,
|
|
110811
|
+
options.onProgress ? (progress) => {
|
|
110812
|
+
options.onProgress?.({
|
|
110813
|
+
...progress,
|
|
110814
|
+
totalFrames: options.totalFrames,
|
|
110815
|
+
capturedFrames: Math.min(
|
|
110816
|
+
options.totalFrames,
|
|
110817
|
+
capturedBeforeBatch + progress.capturedFrames
|
|
110818
|
+
)
|
|
110819
|
+
});
|
|
110820
|
+
} : void 0,
|
|
110821
|
+
void 0,
|
|
110822
|
+
options.cfg
|
|
110823
|
+
);
|
|
110824
|
+
} finally {
|
|
110825
|
+
await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
|
|
110826
|
+
}
|
|
110827
|
+
}
|
|
110828
|
+
const remaining = findMissingFrameRanges(
|
|
110829
|
+
options.totalFrames,
|
|
110830
|
+
options.framesDir,
|
|
110831
|
+
options.frameExt
|
|
110832
|
+
);
|
|
110833
|
+
if (remaining.length === 0) {
|
|
110834
|
+
return attempts;
|
|
110835
|
+
}
|
|
110836
|
+
if (!options.allowRetry || currentWorkers <= 1) {
|
|
110837
|
+
throw new Error(
|
|
110838
|
+
`[Render] Capture completed but ${countFrameRanges(remaining)} frame(s) are missing`
|
|
110839
|
+
);
|
|
110840
|
+
}
|
|
110841
|
+
const nextWorkers = getNextRetryWorkerCount(currentWorkers);
|
|
110842
|
+
options.log.warn("[Render] Retrying missing captured frames with fewer workers.", {
|
|
110843
|
+
fromWorkers: currentWorkers,
|
|
110844
|
+
toWorkers: nextWorkers,
|
|
110845
|
+
missingFrames: countFrameRanges(remaining)
|
|
110846
|
+
});
|
|
110847
|
+
currentWorkers = nextWorkers;
|
|
110848
|
+
missingRanges = remaining;
|
|
110849
|
+
attempt++;
|
|
110850
|
+
} catch (error) {
|
|
110851
|
+
const remaining = findMissingFrameRanges(
|
|
110852
|
+
options.totalFrames,
|
|
110853
|
+
options.framesDir,
|
|
110854
|
+
options.frameExt
|
|
110855
|
+
);
|
|
110856
|
+
if (remaining.length === 0) {
|
|
110857
|
+
return attempts;
|
|
110858
|
+
}
|
|
110859
|
+
if (!options.allowRetry || currentWorkers <= 1 || !isRecoverableParallelCaptureError(error)) {
|
|
110860
|
+
throw error;
|
|
110861
|
+
}
|
|
110862
|
+
const nextWorkers = getNextRetryWorkerCount(currentWorkers);
|
|
110863
|
+
options.log.warn("[Render] Parallel capture timed out; retrying missing frames.", {
|
|
110864
|
+
fromWorkers: currentWorkers,
|
|
110865
|
+
toWorkers: nextWorkers,
|
|
110866
|
+
missingFrames: countFrameRanges(remaining),
|
|
110867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110868
|
+
});
|
|
110869
|
+
currentWorkers = nextWorkers;
|
|
110870
|
+
missingRanges = remaining;
|
|
110871
|
+
attempt++;
|
|
110872
|
+
}
|
|
110873
|
+
}
|
|
110874
|
+
}
|
|
110568
110875
|
function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer) {
|
|
110569
110876
|
const frameDir = hdrFrameDirs.get(el.id);
|
|
110570
110877
|
const startTime = hdrStartTimes.get(el.id);
|
|
@@ -111191,7 +111498,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111191
111498
|
let extractionResult = null;
|
|
111192
111499
|
const nativeHdrVideoIds = /* @__PURE__ */ new Set();
|
|
111193
111500
|
const videoTransfers = /* @__PURE__ */ new Map();
|
|
111194
|
-
if (job.config.
|
|
111501
|
+
if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
|
|
111195
111502
|
await Promise.all(
|
|
111196
111503
|
composition.videos.map(async (v) => {
|
|
111197
111504
|
let videoPath = v.src;
|
|
@@ -111212,7 +111519,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111212
111519
|
const imageTransfers = /* @__PURE__ */ new Map();
|
|
111213
111520
|
const hdrImageSrcPaths = /* @__PURE__ */ new Map();
|
|
111214
111521
|
const imageColorSpaces = [];
|
|
111215
|
-
if (job.config.
|
|
111522
|
+
if (job.config.hdrMode !== "force-sdr" && composition.images.length > 0) {
|
|
111216
111523
|
const probed = await Promise.all(
|
|
111217
111524
|
composition.images.map(async (img) => {
|
|
111218
111525
|
let imgPath = img.src;
|
|
@@ -111269,28 +111576,53 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111269
111576
|
perfStages.videoExtractMs = Date.now() - stage2Start;
|
|
111270
111577
|
}
|
|
111271
111578
|
let effectiveHdr;
|
|
111272
|
-
|
|
111579
|
+
let forcedHdrWithoutSources = false;
|
|
111580
|
+
{
|
|
111581
|
+
const hdrMode = job.config.hdrMode ?? "auto";
|
|
111273
111582
|
const videoColorSpaces = (extractionResult?.extracted ?? []).map(
|
|
111274
111583
|
(ext) => ext.metadata.colorSpace
|
|
111275
111584
|
);
|
|
111276
111585
|
const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
|
|
111277
|
-
|
|
111278
|
-
|
|
111279
|
-
|
|
111586
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
111587
|
+
if (hdrMode === "force-sdr") {
|
|
111588
|
+
effectiveHdr = void 0;
|
|
111589
|
+
} else if (hdrMode === "force-hdr") {
|
|
111590
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
111591
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
111592
|
+
} else {
|
|
111593
|
+
effectiveHdr = { transfer: "hlg" };
|
|
111594
|
+
forcedHdrWithoutSources = true;
|
|
111595
|
+
}
|
|
111596
|
+
} else {
|
|
111597
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
111280
111598
|
effectiveHdr = { transfer: info.dominantTransfer };
|
|
111281
111599
|
}
|
|
111282
111600
|
}
|
|
111283
111601
|
}
|
|
111284
111602
|
if (effectiveHdr && outputFormat !== "mp4") {
|
|
111603
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
111285
111604
|
log.warn(
|
|
111286
|
-
`[Render]
|
|
111605
|
+
`[Render] ${hdrSourceReason}, but format is "${outputFormat}" \u2014 falling back to SDR. HDR + alpha is not supported. Use --format mp4 for HDR10 output.`
|
|
111287
111606
|
);
|
|
111288
111607
|
effectiveHdr = void 0;
|
|
111289
111608
|
}
|
|
111290
|
-
|
|
111291
|
-
|
|
111292
|
-
|
|
111293
|
-
|
|
111609
|
+
{
|
|
111610
|
+
const hdrMode = job.config.hdrMode ?? "auto";
|
|
111611
|
+
if (forcedHdrWithoutSources) {
|
|
111612
|
+
log.warn(
|
|
111613
|
+
"[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."
|
|
111614
|
+
);
|
|
111615
|
+
}
|
|
111616
|
+
if (effectiveHdr) {
|
|
111617
|
+
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)";
|
|
111618
|
+
log.info(
|
|
111619
|
+
`[Render] HDR ${reason} \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
111620
|
+
);
|
|
111621
|
+
} else if (hdrMode === "force-sdr") {
|
|
111622
|
+
log.info("[Render] SDR forced by --sdr flag");
|
|
111623
|
+
} else {
|
|
111624
|
+
log.info("[Render] No HDR sources detected \u2014 rendering SDR");
|
|
111625
|
+
}
|
|
111294
111626
|
}
|
|
111295
111627
|
const stage3Start = Date.now();
|
|
111296
111628
|
updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
|
|
@@ -111337,7 +111669,101 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111337
111669
|
...captureOptions,
|
|
111338
111670
|
skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
|
|
111339
111671
|
});
|
|
111340
|
-
|
|
111672
|
+
let captureCalibration;
|
|
111673
|
+
let switchedToScreenshotAfterCalibration = false;
|
|
111674
|
+
if (job.config.workers === void 0 && totalFrames >= 60) {
|
|
111675
|
+
const calibrationDir = join16(workDir, "capture-calibration");
|
|
111676
|
+
const calibrationCfg = createCaptureCalibrationConfig(cfg);
|
|
111677
|
+
const videoInjector = createVideoFrameInjector(frameLookup);
|
|
111678
|
+
let calibrationSession = null;
|
|
111679
|
+
try {
|
|
111680
|
+
calibrationSession = await createCaptureSession(
|
|
111681
|
+
fileServer.url,
|
|
111682
|
+
calibrationDir,
|
|
111683
|
+
buildHdrCaptureOptions(),
|
|
111684
|
+
videoInjector,
|
|
111685
|
+
calibrationCfg
|
|
111686
|
+
);
|
|
111687
|
+
if (!calibrationSession.isInitialized) {
|
|
111688
|
+
await initializeSession(calibrationSession);
|
|
111689
|
+
}
|
|
111690
|
+
assertNotAborted();
|
|
111691
|
+
captureCalibration = await measureCaptureCostFromSession(
|
|
111692
|
+
calibrationSession,
|
|
111693
|
+
totalFrames,
|
|
111694
|
+
job.config.fps
|
|
111695
|
+
);
|
|
111696
|
+
if (captureCalibration.estimate.multiplier > 1) {
|
|
111697
|
+
log.warn("[Render] Measured slow frame capture during auto-worker calibration.", {
|
|
111698
|
+
multiplier: captureCalibration.estimate.multiplier,
|
|
111699
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
111700
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
|
|
111701
|
+
});
|
|
111702
|
+
} else {
|
|
111703
|
+
log.debug("[Render] Auto-worker calibration kept baseline capture cost.", {
|
|
111704
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
111705
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
|
|
111706
|
+
});
|
|
111707
|
+
}
|
|
111708
|
+
} catch (error) {
|
|
111709
|
+
const shouldFallbackToScreenshot = !cfg.forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
111710
|
+
if (shouldFallbackToScreenshot) {
|
|
111711
|
+
cfg.forceScreenshot = true;
|
|
111712
|
+
switchedToScreenshotAfterCalibration = true;
|
|
111713
|
+
if (probeSession) {
|
|
111714
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111715
|
+
await closeCaptureSession(probeSession).catch(() => {
|
|
111716
|
+
});
|
|
111717
|
+
probeSession = null;
|
|
111718
|
+
}
|
|
111719
|
+
}
|
|
111720
|
+
captureCalibration = {
|
|
111721
|
+
estimate: {
|
|
111722
|
+
multiplier: MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
|
|
111723
|
+
reasons: shouldFallbackToScreenshot ? ["calibration-beginframe-timeout", "screenshot-fallback"] : ["calibration-failed"]
|
|
111724
|
+
},
|
|
111725
|
+
samples: []
|
|
111726
|
+
};
|
|
111727
|
+
if (shouldFallbackToScreenshot) {
|
|
111728
|
+
log.warn(
|
|
111729
|
+
"[Render] BeginFrame auto-worker calibration timed out; falling back to screenshot capture mode.",
|
|
111730
|
+
{
|
|
111731
|
+
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
111732
|
+
error: error instanceof Error ? error.message : String(error)
|
|
111733
|
+
}
|
|
111734
|
+
);
|
|
111735
|
+
} else {
|
|
111736
|
+
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
111737
|
+
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
111738
|
+
error: error instanceof Error ? error.message : String(error)
|
|
111739
|
+
});
|
|
111740
|
+
}
|
|
111741
|
+
} finally {
|
|
111742
|
+
if (calibrationSession) {
|
|
111743
|
+
lastBrowserConsole = calibrationSession.browserConsoleBuffer;
|
|
111744
|
+
await closeCaptureSession(calibrationSession).catch(() => {
|
|
111745
|
+
});
|
|
111746
|
+
}
|
|
111747
|
+
}
|
|
111748
|
+
}
|
|
111749
|
+
let workerCount = resolveRenderWorkerCount(
|
|
111750
|
+
totalFrames,
|
|
111751
|
+
job.config.workers,
|
|
111752
|
+
cfg,
|
|
111753
|
+
compiled,
|
|
111754
|
+
composition,
|
|
111755
|
+
log,
|
|
111756
|
+
captureCalibration?.estimate
|
|
111757
|
+
);
|
|
111758
|
+
if (switchedToScreenshotAfterCalibration && workerCount > 1) {
|
|
111759
|
+
workerCount = 1;
|
|
111760
|
+
}
|
|
111761
|
+
if (workerCount > 1 && probeSession) {
|
|
111762
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111763
|
+
await closeCaptureSession(probeSession);
|
|
111764
|
+
probeSession = null;
|
|
111765
|
+
}
|
|
111766
|
+
const captureAttempts = [];
|
|
111341
111767
|
const FORMAT_EXT = {
|
|
111342
111768
|
mp4: ".mp4",
|
|
111343
111769
|
webm: ".webm",
|
|
@@ -111929,15 +112355,18 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111929
112355
|
perfStages.encodeMs = encodeResult.durationMs;
|
|
111930
112356
|
} else {
|
|
111931
112357
|
if (workerCount > 1) {
|
|
111932
|
-
const
|
|
111933
|
-
|
|
111934
|
-
fileServer.url,
|
|
112358
|
+
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
112359
|
+
serverUrl: fileServer.url,
|
|
111935
112360
|
workDir,
|
|
111936
|
-
|
|
111937
|
-
|
|
111938
|
-
|
|
112361
|
+
framesDir,
|
|
112362
|
+
totalFrames: job.totalFrames,
|
|
112363
|
+
initialWorkerCount: workerCount,
|
|
112364
|
+
allowRetry: job.config.workers === void 0,
|
|
112365
|
+
frameExt: needsAlpha ? "png" : "jpg",
|
|
112366
|
+
captureOptions: buildHdrCaptureOptions(),
|
|
112367
|
+
createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
|
|
111939
112368
|
abortSignal,
|
|
111940
|
-
(progress) => {
|
|
112369
|
+
onProgress: (progress) => {
|
|
111941
112370
|
job.framesRendered = progress.capturedFrames;
|
|
111942
112371
|
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
111943
112372
|
const progressPct = 25 + frameProgress * 45;
|
|
@@ -111945,16 +112374,20 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
111945
112374
|
updateJobStatus(
|
|
111946
112375
|
job,
|
|
111947
112376
|
"rendering",
|
|
111948
|
-
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${
|
|
112377
|
+
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
111949
112378
|
Math.round(progressPct),
|
|
111950
112379
|
onProgress
|
|
111951
112380
|
);
|
|
111952
112381
|
}
|
|
111953
112382
|
},
|
|
111954
|
-
|
|
111955
|
-
|
|
111956
|
-
);
|
|
111957
|
-
|
|
112383
|
+
cfg,
|
|
112384
|
+
log
|
|
112385
|
+
});
|
|
112386
|
+
captureAttempts.push(...attempts);
|
|
112387
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
112388
|
+
if (lastAttempt) {
|
|
112389
|
+
workerCount = lastAttempt.workers;
|
|
112390
|
+
}
|
|
111958
112391
|
if (probeSession) {
|
|
111959
112392
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
111960
112393
|
await closeCaptureSession(probeSession);
|
|
@@ -112124,6 +112557,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
112124
112557
|
stages: perfStages,
|
|
112125
112558
|
videoExtractBreakdown: extractionResult?.phaseBreakdown,
|
|
112126
112559
|
tmpPeakBytes,
|
|
112560
|
+
captureCalibration: captureCalibration ? {
|
|
112561
|
+
sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex),
|
|
112562
|
+
p95Ms: captureCalibration.estimate.p95Ms,
|
|
112563
|
+
multiplier: captureCalibration.estimate.multiplier,
|
|
112564
|
+
reasons: captureCalibration.estimate.reasons
|
|
112565
|
+
} : void 0,
|
|
112566
|
+
captureAttempts: captureAttempts.length > 0 ? captureAttempts : void 0,
|
|
112127
112567
|
hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
|
|
112128
112568
|
captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0,
|
|
112129
112569
|
peakRssMb: Math.round(peakRssBytes / (1024 * 1024)),
|