@hyperframes/producer 0.8.199 → 0.8.201
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/README.md +8 -8
- package/dist/distributed.js +105 -33
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +66 -62
- package/dist/index.js +175 -43
- package/dist/public-server.js +141 -38
- package/dist/regression-harness.d.ts +53 -0
- package/dist/services/distributed/chunkWeights.d.ts +2 -0
- package/dist/services/htmlCompiler.d.ts +0 -2
- package/dist/services/render/audioPadTrim.d.ts +2 -0
- package/dist/services/render/stages/captureStage.d.ts +1 -0
- package/dist/services/render/stages/compileStage.d.ts +1 -1
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/dist/services/renderMediaCollector.d.ts +2 -0
- package/dist/services/renderOrchestrator.d.ts +37 -45
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
createVideoFrameInjector,
|
|
34
34
|
getEncoderPreset as getEncoderPreset3,
|
|
35
35
|
distributeFrames as distributeFrames2,
|
|
36
|
+
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
36
37
|
executeParallelCapture as executeParallelCapture2,
|
|
37
38
|
mergeWorkerFrames,
|
|
38
39
|
getSystemTotalMb,
|
|
@@ -2094,6 +2095,7 @@ function aggregateDrawElement(perfs, de) {
|
|
|
2094
2095
|
blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
|
|
2095
2096
|
blankRecaptures: drain?.blankRecaptures ?? 0,
|
|
2096
2097
|
boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
|
|
2098
|
+
riskFrames: perfs.reduce((sum2, p) => sum2 + (p.deRiskFrames ?? 0), 0),
|
|
2097
2099
|
ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
|
|
2098
2100
|
frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
|
|
2099
2101
|
};
|
|
@@ -3730,11 +3732,21 @@ function createDrainFrameGuard(args) {
|
|
|
3730
3732
|
return buf;
|
|
3731
3733
|
};
|
|
3732
3734
|
}
|
|
3733
|
-
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
|
|
3735
|
+
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
|
|
3734
3736
|
let prev = null;
|
|
3735
3737
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
3736
3738
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
|
3737
|
-
const guardFrame = (
|
|
3739
|
+
const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
|
|
3740
|
+
const captureRisk = async (idx) => {
|
|
3741
|
+
await risk.open();
|
|
3742
|
+
lastProgressAt = Date.now();
|
|
3743
|
+
return raceAgainstStall(
|
|
3744
|
+
risk.capture(idx, frameTime(idx)),
|
|
3745
|
+
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3746
|
+
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3747
|
+
abortSignal
|
|
3748
|
+
);
|
|
3749
|
+
};
|
|
3738
3750
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
3739
3751
|
let lastProgressAt = Date.now();
|
|
3740
3752
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
@@ -3746,7 +3758,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3746
3758
|
const drainPrev = async () => {
|
|
3747
3759
|
if (!prev) return;
|
|
3748
3760
|
assertNotAborted();
|
|
3749
|
-
const buf = await guardFrame(prev
|
|
3761
|
+
const buf = await guardFrame(prev, await prev.encodeResult);
|
|
3750
3762
|
await reorderBuffer.waitForFrame(prev.idx);
|
|
3751
3763
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
|
|
3752
3764
|
reorderBuffer.advanceTo(prev.idx + 1);
|
|
@@ -3766,7 +3778,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3766
3778
|
const drainBatch = async (batch) => {
|
|
3767
3779
|
for (const item of batch) {
|
|
3768
3780
|
assertNotAborted();
|
|
3769
|
-
const buf = await guardFrame(item
|
|
3781
|
+
const buf = await guardFrame(item, await item.encodeResult);
|
|
3770
3782
|
await reorderBuffer.waitForFrame(item.idx);
|
|
3771
3783
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
|
|
3772
3784
|
reorderBuffer.advanceTo(item.idx + 1);
|
|
@@ -3784,7 +3796,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3784
3796
|
};
|
|
3785
3797
|
if (batchN > 1 && !session.onBeforeCapture) {
|
|
3786
3798
|
const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
|
|
3787
|
-
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
|
|
3799
|
+
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
|
|
3788
3800
|
let prevBatch = [];
|
|
3789
3801
|
let i = 0;
|
|
3790
3802
|
while (i < totalFrames) {
|
|
@@ -3801,6 +3813,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3801
3813
|
);
|
|
3802
3814
|
await drainBatch(prevBatch);
|
|
3803
3815
|
prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
|
|
3816
|
+
} else if (risk?.frames.has(i)) {
|
|
3817
|
+
const buffer = await captureRisk(i);
|
|
3818
|
+
await drainBatch(prevBatch);
|
|
3819
|
+
prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
|
|
3820
|
+
i++;
|
|
3804
3821
|
} else {
|
|
3805
3822
|
const { encodeResult } = await captureWithStallGuard(
|
|
3806
3823
|
i,
|
|
@@ -3816,6 +3833,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3816
3833
|
}
|
|
3817
3834
|
for (let i = 0; i < totalFrames; i++) {
|
|
3818
3835
|
assertNotAborted();
|
|
3836
|
+
if (risk?.frames.has(i)) {
|
|
3837
|
+
const buffer = await captureRisk(i);
|
|
3838
|
+
await drainPrev();
|
|
3839
|
+
prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
|
|
3840
|
+
continue;
|
|
3841
|
+
}
|
|
3819
3842
|
const time = frameTime(i);
|
|
3820
3843
|
const { encodeResult } = await captureWithStallGuard(
|
|
3821
3844
|
i,
|
|
@@ -4062,6 +4085,24 @@ async function runCaptureStreamingStage(input) {
|
|
|
4062
4085
|
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4063
4086
|
probeSession = null;
|
|
4064
4087
|
}
|
|
4088
|
+
let riskSession = null;
|
|
4089
|
+
const openRiskSession = async () => {
|
|
4090
|
+
const opened = await createCaptureSession(
|
|
4091
|
+
fileServer.url,
|
|
4092
|
+
framesDir,
|
|
4093
|
+
buildCaptureOptions(),
|
|
4094
|
+
createRenderVideoFrameInjector(),
|
|
4095
|
+
{
|
|
4096
|
+
...captureCfg,
|
|
4097
|
+
useDrawElement: false,
|
|
4098
|
+
staticFrameDedup: false,
|
|
4099
|
+
enableBrowserPool: false
|
|
4100
|
+
}
|
|
4101
|
+
);
|
|
4102
|
+
riskSession = opened;
|
|
4103
|
+
await initializeSession(opened);
|
|
4104
|
+
return opened;
|
|
4105
|
+
};
|
|
4065
4106
|
try {
|
|
4066
4107
|
if (!session.isInitialized) {
|
|
4067
4108
|
await initializeSession(session);
|
|
@@ -4076,6 +4117,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4076
4117
|
blankDeterministicAccepts: 0,
|
|
4077
4118
|
blankRecaptures: 0
|
|
4078
4119
|
};
|
|
4120
|
+
const riskFrames = session.deRiskFrames;
|
|
4079
4121
|
await runWorkerEncodePipelineLoop(
|
|
4080
4122
|
session,
|
|
4081
4123
|
totalFrames,
|
|
@@ -4086,7 +4128,16 @@ async function runCaptureStreamingStage(input) {
|
|
|
4086
4128
|
onProgress,
|
|
4087
4129
|
log,
|
|
4088
4130
|
deDrainStats,
|
|
4089
|
-
abortSignal
|
|
4131
|
+
abortSignal,
|
|
4132
|
+
riskFrames && riskFrames.size > 0 ? {
|
|
4133
|
+
frames: riskFrames,
|
|
4134
|
+
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4135
|
+
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4136
|
+
open: async () => {
|
|
4137
|
+
riskSession ??= await openRiskSession();
|
|
4138
|
+
},
|
|
4139
|
+
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4140
|
+
} : void 0
|
|
4090
4141
|
);
|
|
4091
4142
|
} else {
|
|
4092
4143
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
@@ -4124,6 +4175,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4124
4175
|
} finally {
|
|
4125
4176
|
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4126
4177
|
await closeCaptureSession(session);
|
|
4178
|
+
if (riskSession) await closeCaptureSession(riskSession);
|
|
4127
4179
|
}
|
|
4128
4180
|
}
|
|
4129
4181
|
const encodeFrom = job.progress;
|
|
@@ -4283,7 +4335,7 @@ function openSegmentedSessionReuse(input, session) {
|
|
|
4283
4335
|
async function openSegmentedSession(input) {
|
|
4284
4336
|
const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
|
|
4285
4337
|
const videoInjector = input.createRenderVideoFrameInjector();
|
|
4286
|
-
const captureOptions = input.buildCaptureOptions();
|
|
4338
|
+
const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
|
|
4287
4339
|
const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
|
|
4288
4340
|
parallel: (input.workerCount ?? 1) > 1
|
|
4289
4341
|
});
|
|
@@ -5528,6 +5580,7 @@ import {
|
|
|
5528
5580
|
parseVideoElements,
|
|
5529
5581
|
parseImageElements,
|
|
5530
5582
|
parseAudioElements,
|
|
5583
|
+
isSelfOrAncestorHidden,
|
|
5531
5584
|
resolveReferencedStart
|
|
5532
5585
|
} from "@hyperframes/engine";
|
|
5533
5586
|
function resolveHostEnd(host, hostStart) {
|
|
@@ -5588,6 +5641,15 @@ function collectHostWindows(html) {
|
|
|
5588
5641
|
}
|
|
5589
5642
|
return windows;
|
|
5590
5643
|
}
|
|
5644
|
+
function collectHiddenVideoIds(html) {
|
|
5645
|
+
const { document: document2 } = parseHTML2(html);
|
|
5646
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5647
|
+
for (const element of document2.querySelectorAll("video")) {
|
|
5648
|
+
const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
|
|
5649
|
+
if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
|
|
5650
|
+
}
|
|
5651
|
+
return hidden;
|
|
5652
|
+
}
|
|
5591
5653
|
function toAbsoluteWindow(start, end, window2) {
|
|
5592
5654
|
const absoluteStart = resolveAbsoluteMediaStartSeconds({
|
|
5593
5655
|
authoredStart: start,
|
|
@@ -5621,8 +5683,13 @@ function collectRenderMedia(html) {
|
|
|
5621
5683
|
const windows = collectHostWindows(html);
|
|
5622
5684
|
const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
|
|
5623
5685
|
const droppedMediaIds = /* @__PURE__ */ new Set();
|
|
5686
|
+
const hiddenIds = collectHiddenVideoIds(html);
|
|
5624
5687
|
const videos = [];
|
|
5625
5688
|
for (const video of parseVideoElements(html)) {
|
|
5689
|
+
if (hiddenIds.has(video.id)) {
|
|
5690
|
+
droppedMediaIds.add(video.id);
|
|
5691
|
+
continue;
|
|
5692
|
+
}
|
|
5626
5693
|
const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
|
|
5627
5694
|
if (!absolute) {
|
|
5628
5695
|
droppedMediaIds.add(video.id);
|
|
@@ -5832,7 +5899,7 @@ async function runAnimatedGifTranscode(request) {
|
|
|
5832
5899
|
failureReason: result.failureReason
|
|
5833
5900
|
});
|
|
5834
5901
|
}
|
|
5835
|
-
if (result.terminationReason === "
|
|
5902
|
+
if (result.terminationReason === "inactivity") {
|
|
5836
5903
|
throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
|
|
5837
5904
|
}
|
|
5838
5905
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
@@ -6299,10 +6366,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
|
|
|
6299
6366
|
function detectThreeDTransformUsage(html) {
|
|
6300
6367
|
return THREE_D_CONTEXT_PATTERN.test(html);
|
|
6301
6368
|
}
|
|
6302
|
-
var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
|
|
6303
|
-
function detectMixBlendModeUsage(html) {
|
|
6304
|
-
return MIX_BLEND_MODE_PATTERN.test(html);
|
|
6305
|
-
}
|
|
6306
6369
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
6307
6370
|
function detectAncestorBackgroundImage(html) {
|
|
6308
6371
|
const { document: document2 } = parseHTML4(html);
|
|
@@ -6455,8 +6518,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6455
6518
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
6456
6519
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
6457
6520
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
6521
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
6458
6522
|
const clampResults = await Promise.all(
|
|
6459
|
-
preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
|
|
6523
|
+
preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
|
|
6460
6524
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
6461
6525
|
el.src,
|
|
6462
6526
|
el.mediaStart,
|
|
@@ -7315,7 +7379,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7315
7379
|
const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
|
|
7316
7380
|
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
7317
7381
|
const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
|
|
7318
|
-
const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
|
|
7319
7382
|
const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
|
|
7320
7383
|
const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
|
|
7321
7384
|
injectTextRenderingRule(
|
|
@@ -7429,7 +7492,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7429
7492
|
renderModeHints,
|
|
7430
7493
|
hasShaderTransitions,
|
|
7431
7494
|
usesThreeDTransforms,
|
|
7432
|
-
usesMixBlendMode,
|
|
7433
7495
|
hasAncestorBackgroundImage
|
|
7434
7496
|
};
|
|
7435
7497
|
}
|
|
@@ -7836,13 +7898,6 @@ async function runCompileStage(input) {
|
|
|
7836
7898
|
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7837
7899
|
);
|
|
7838
7900
|
}
|
|
7839
|
-
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
7840
|
-
cfg.useDrawElement = false;
|
|
7841
|
-
deCompileGate = "mix_blend_mode";
|
|
7842
|
-
log.info(
|
|
7843
|
-
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7844
|
-
);
|
|
7845
|
-
}
|
|
7846
7901
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
7847
7902
|
cfg.useDrawElement = false;
|
|
7848
7903
|
deCompileGate = "ancestor_background_image";
|
|
@@ -8136,6 +8191,7 @@ async function runProbeStage(input) {
|
|
|
8136
8191
|
height,
|
|
8137
8192
|
fps: job.config.fps,
|
|
8138
8193
|
format: captureFormat,
|
|
8194
|
+
transparent: needsAlpha,
|
|
8139
8195
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
8140
8196
|
variables: job.config.variables,
|
|
8141
8197
|
deviceScaleFactor,
|
|
@@ -9273,10 +9329,11 @@ async function constrainAacTruePeak(input) {
|
|
|
9273
9329
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
9274
9330
|
return enforceAacTruePeak({
|
|
9275
9331
|
audioPath: input.audioPath,
|
|
9332
|
+
sourcePath: input.sourcePath,
|
|
9276
9333
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
9277
9334
|
signal: input.signal,
|
|
9278
|
-
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
9279
|
-
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9335
|
+
probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
|
|
9336
|
+
encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9280
9337
|
});
|
|
9281
9338
|
}
|
|
9282
9339
|
async function enforceAacTruePeak(input) {
|
|
@@ -9286,6 +9343,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9286
9343
|
const correctedPath = join18(scratchDir, "audio.m4a");
|
|
9287
9344
|
const measurements = [];
|
|
9288
9345
|
let attenuationDb = 0;
|
|
9346
|
+
let sourcePeakDbfs;
|
|
9289
9347
|
let measuredPath = input.audioPath;
|
|
9290
9348
|
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
9291
9349
|
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
@@ -9302,7 +9360,11 @@ async function enforceAacTruePeak(input) {
|
|
|
9302
9360
|
}
|
|
9303
9361
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
9304
9362
|
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
9305
|
-
|
|
9363
|
+
if (input.sourcePath && sourcePeakDbfs === void 0) {
|
|
9364
|
+
sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
|
|
9365
|
+
}
|
|
9366
|
+
const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
|
|
9367
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
|
|
9306
9368
|
const result = await input.encode(
|
|
9307
9369
|
input.audioPath,
|
|
9308
9370
|
correctedPath,
|
|
@@ -9738,7 +9800,7 @@ async function runCaptureStage(input) {
|
|
|
9738
9800
|
);
|
|
9739
9801
|
}
|
|
9740
9802
|
}
|
|
9741
|
-
const captureOptions = buildCaptureOptions();
|
|
9803
|
+
const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
|
|
9742
9804
|
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
9743
9805
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
9744
9806
|
framesDir,
|
|
@@ -9759,6 +9821,7 @@ async function runCaptureStage(input) {
|
|
|
9759
9821
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
9760
9822
|
abortSignal,
|
|
9761
9823
|
frameRangeStart: frameRange?.startFrame,
|
|
9824
|
+
interleaveFrames: input.interleaveFrames,
|
|
9762
9825
|
dedupPerfs,
|
|
9763
9826
|
pageErrorLines: workerPageErrors,
|
|
9764
9827
|
onProgress: (progress) => {
|
|
@@ -11274,9 +11337,6 @@ async function runEncodeStage(input) {
|
|
|
11274
11337
|
return { encodeMs: Date.now() - stage5Start };
|
|
11275
11338
|
}
|
|
11276
11339
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
11277
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
11278
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
11279
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
11280
11340
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
11281
11341
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
11282
11342
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -11287,7 +11347,7 @@ async function runEncodeStage(input) {
|
|
|
11287
11347
|
encoderOpts,
|
|
11288
11348
|
chunkedEncodeSize,
|
|
11289
11349
|
abortSignal,
|
|
11290
|
-
|
|
11350
|
+
engineCfg,
|
|
11291
11351
|
onFramesEncoded
|
|
11292
11352
|
) : await encodeFramesFromDir(
|
|
11293
11353
|
framesDir,
|
|
@@ -11295,7 +11355,7 @@ async function runEncodeStage(input) {
|
|
|
11295
11355
|
videoOnlyPath,
|
|
11296
11356
|
encoderOpts,
|
|
11297
11357
|
abortSignal,
|
|
11298
|
-
|
|
11358
|
+
engineCfg,
|
|
11299
11359
|
onFramesEncoded
|
|
11300
11360
|
);
|
|
11301
11361
|
assertNotAborted();
|
|
@@ -11637,16 +11697,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11637
11697
|
}
|
|
11638
11698
|
return ranges;
|
|
11639
11699
|
}
|
|
11640
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
11700
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11641
11701
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11642
11702
|
const batches = [];
|
|
11703
|
+
const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11704
|
+
if (stride > 1) {
|
|
11705
|
+
const spans = /* @__PURE__ */ new Map();
|
|
11706
|
+
for (const range of ranges) {
|
|
11707
|
+
for (let frame = range.startFrame; frame < range.endFrame; frame++) {
|
|
11708
|
+
const span = spans.get(frame % stride);
|
|
11709
|
+
if (span) span.last = frame;
|
|
11710
|
+
else spans.set(frame % stride, { first: frame, last: frame });
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
const strided = [...spans.values()];
|
|
11714
|
+
for (let i = 0; i < strided.length; i += workersPerBatch) {
|
|
11715
|
+
const batchIndex = batches.length;
|
|
11716
|
+
batches.push(
|
|
11717
|
+
strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
|
|
11718
|
+
workerId,
|
|
11719
|
+
startFrame: rangeStart + first,
|
|
11720
|
+
endFrame: rangeStart + last + 1,
|
|
11721
|
+
frameStride: stride,
|
|
11722
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11723
|
+
outputFrameOffset: rangeStart
|
|
11724
|
+
}))
|
|
11725
|
+
);
|
|
11726
|
+
}
|
|
11727
|
+
return batches;
|
|
11728
|
+
}
|
|
11643
11729
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
11644
11730
|
const batchIndex = batches.length;
|
|
11645
11731
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
11646
11732
|
workerId,
|
|
11647
11733
|
startFrame: rangeStart + range.startFrame,
|
|
11648
11734
|
endFrame: rangeStart + range.endFrame,
|
|
11649
|
-
outputDir:
|
|
11735
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11650
11736
|
outputFrameOffset: rangeStart
|
|
11651
11737
|
}));
|
|
11652
11738
|
batches.push(batch);
|
|
@@ -11739,8 +11825,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11739
11825
|
currentWorkers,
|
|
11740
11826
|
attemptWorkDir,
|
|
11741
11827
|
attempt,
|
|
11742
|
-
rangeStart
|
|
11743
|
-
|
|
11828
|
+
rangeStart,
|
|
11829
|
+
options.interleaveFrames ? options.initialWorkerCount : 1
|
|
11830
|
+
) : [
|
|
11831
|
+
(options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
|
|
11832
|
+
options.totalFrames,
|
|
11833
|
+
currentWorkers,
|
|
11834
|
+
attemptWorkDir,
|
|
11835
|
+
rangeStart
|
|
11836
|
+
)
|
|
11837
|
+
];
|
|
11744
11838
|
options.dedupPerfs.length = 0;
|
|
11745
11839
|
try {
|
|
11746
11840
|
for (const tasks of batches) {
|
|
@@ -12192,6 +12286,9 @@ function resolveDeShortBand(args) {
|
|
|
12192
12286
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
12193
12287
|
return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
|
|
12194
12288
|
}
|
|
12289
|
+
function probeNeedsScreenshotFrames(session) {
|
|
12290
|
+
return (session?.deRiskFrames?.size ?? 0) > 0;
|
|
12291
|
+
}
|
|
12195
12292
|
function isProbeDeGated(session) {
|
|
12196
12293
|
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
12197
12294
|
}
|
|
@@ -12849,6 +12946,7 @@ async function executeRenderPipeline(input) {
|
|
|
12849
12946
|
height,
|
|
12850
12947
|
fps: job.config.fps,
|
|
12851
12948
|
format: captureImageFormat,
|
|
12949
|
+
transparent: needsAlpha,
|
|
12852
12950
|
quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
12853
12951
|
variables: job.config.variables,
|
|
12854
12952
|
deviceScaleFactor,
|
|
@@ -12870,6 +12968,7 @@ async function executeRenderPipeline(input) {
|
|
|
12870
12968
|
// Probe-resolved duration: drawElement self-verification derives its
|
|
12871
12969
|
// sample frame indices from this so they land inside the drained range.
|
|
12872
12970
|
compositionDurationSeconds: job.duration,
|
|
12971
|
+
deFrameFallback: true,
|
|
12873
12972
|
requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
|
|
12874
12973
|
// Live route for Chrome memory (spec §5 Phase −1). The aggregate route
|
|
12875
12974
|
// through CapturePerfSummary only exists on success; a mid-capture
|
|
@@ -12970,7 +13069,7 @@ async function executeRenderPipeline(input) {
|
|
|
12970
13069
|
minFrames: deParallelMinFrames,
|
|
12971
13070
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12972
13071
|
supersampling: deviceScaleFactor !== 1,
|
|
12973
|
-
probeDeGated: isProbeDeGated(probeSession),
|
|
13072
|
+
probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
|
|
12974
13073
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12975
13074
|
routerEnabled: deParallelRouterEnabled,
|
|
12976
13075
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -13012,6 +13111,8 @@ async function executeRenderPipeline(input) {
|
|
|
13012
13111
|
deInversionEligible = false;
|
|
13013
13112
|
deParallelRouterEligible = false;
|
|
13014
13113
|
deShortBand = void 0;
|
|
13114
|
+
} else if (probeNeedsScreenshotFrames(probeSession)) {
|
|
13115
|
+
deParallelRouterEligible = false;
|
|
13015
13116
|
}
|
|
13016
13117
|
}
|
|
13017
13118
|
const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
|
|
@@ -13203,7 +13304,8 @@ async function executeRenderPipeline(input) {
|
|
|
13203
13304
|
deParallelStreamForced
|
|
13204
13305
|
);
|
|
13205
13306
|
const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
|
|
13206
|
-
|
|
13307
|
+
const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
|
|
13308
|
+
if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
|
|
13207
13309
|
useDrawElement: cfg.useDrawElement,
|
|
13208
13310
|
fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
|
|
13209
13311
|
useStreamingEncode,
|
|
@@ -13877,7 +13979,8 @@ async function executeRenderPipeline(input) {
|
|
|
13877
13979
|
abortSignal: executionSignal,
|
|
13878
13980
|
assertNotAborted,
|
|
13879
13981
|
onProgress,
|
|
13880
|
-
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13982
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13983
|
+
interleaveFrames: overlap !== null
|
|
13881
13984
|
})
|
|
13882
13985
|
);
|
|
13883
13986
|
let captureRes;
|
|
@@ -14827,6 +14930,18 @@ function planFrameWeights(input) {
|
|
|
14827
14930
|
}
|
|
14828
14931
|
return weights;
|
|
14829
14932
|
}
|
|
14933
|
+
function withChunkCaptureCost(slices, weights) {
|
|
14934
|
+
const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
|
|
14935
|
+
return slices.map((slice) => {
|
|
14936
|
+
let sum2 = 0;
|
|
14937
|
+
for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
|
|
14938
|
+
const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
|
|
14939
|
+
return cost > 1 ? { ...slice, captureCost: cost } : slice;
|
|
14940
|
+
});
|
|
14941
|
+
}
|
|
14942
|
+
function effectiveChunkFrames(frames, captureCost = 1) {
|
|
14943
|
+
return Math.round(frames * captureCost);
|
|
14944
|
+
}
|
|
14830
14945
|
function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
|
|
14831
14946
|
if (isUniformFrameCost(weights)) return null;
|
|
14832
14947
|
const totalFrames = weights.length;
|
|
@@ -16147,7 +16262,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
|
|
|
16147
16262
|
MIN_CHUNK_SIZE,
|
|
16148
16263
|
Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
|
|
16149
16264
|
) : null;
|
|
16150
|
-
return
|
|
16265
|
+
return withChunkCaptureCost(
|
|
16266
|
+
weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
|
|
16267
|
+
frameWeights
|
|
16268
|
+
);
|
|
16151
16269
|
}
|
|
16152
16270
|
async function readFontSnapshotSha() {
|
|
16153
16271
|
const module = await import("@hyperframes/core/fonts/embedded-data");
|
|
@@ -18514,7 +18632,12 @@ function parseChunkSlices(value) {
|
|
|
18514
18632
|
throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
|
|
18515
18633
|
}
|
|
18516
18634
|
indexes.add(index);
|
|
18517
|
-
return { index, startFrame, endFrame };
|
|
18635
|
+
if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
|
|
18636
|
+
const captureCost = chunk.captureCost;
|
|
18637
|
+
if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
|
|
18638
|
+
throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
|
|
18639
|
+
}
|
|
18640
|
+
return { index, startFrame, endFrame, captureCost };
|
|
18518
18641
|
});
|
|
18519
18642
|
}
|
|
18520
18643
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
@@ -19691,15 +19814,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19691
19814
|
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
19692
19815
|
)
|
|
19693
19816
|
});
|
|
19817
|
+
const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
|
|
19694
19818
|
const chunkWorkerCount = resolveChunkCaptureWorkers(
|
|
19695
|
-
|
|
19696
|
-
calculateOptimalWorkers2(
|
|
19819
|
+
captureFrames,
|
|
19820
|
+
calculateOptimalWorkers2(captureFrames, void 0, cfg),
|
|
19697
19821
|
options?.maxCaptureWorkers
|
|
19698
19822
|
);
|
|
19699
19823
|
let session = null;
|
|
19700
19824
|
let outputKind;
|
|
19701
19825
|
let framesEncoded = 0;
|
|
19702
19826
|
let sessionBootMs = 0;
|
|
19827
|
+
let videoExtractMs = 0;
|
|
19703
19828
|
let captureStageMs = 0;
|
|
19704
19829
|
let encodeStageMs = 0;
|
|
19705
19830
|
let captureMode;
|
|
@@ -19765,6 +19890,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19765
19890
|
session = null;
|
|
19766
19891
|
}
|
|
19767
19892
|
}
|
|
19893
|
+
const extractStarted = Date.now();
|
|
19768
19894
|
const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
|
|
19769
19895
|
planDir,
|
|
19770
19896
|
planVideos,
|
|
@@ -19774,6 +19900,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19774
19900
|
cfg,
|
|
19775
19901
|
remoteSources,
|
|
19776
19902
|
shownBoxes
|
|
19903
|
+
}).then((frames) => {
|
|
19904
|
+
videoExtractMs = Date.now() - extractStarted;
|
|
19905
|
+
return frames;
|
|
19777
19906
|
}) : Promise.resolve([]);
|
|
19778
19907
|
extraction = deferredVideoFrames;
|
|
19779
19908
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
|
|
@@ -19902,7 +20031,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19902
20031
|
assertNotAborted: () => {
|
|
19903
20032
|
},
|
|
19904
20033
|
frameRange: range,
|
|
19905
|
-
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
|
|
20034
|
+
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
20035
|
+
interleaveFrames: overlap.current !== null
|
|
19906
20036
|
}).catch(async (captureError) => {
|
|
19907
20037
|
await overlap.current?.abort();
|
|
19908
20038
|
overlap.current = null;
|
|
@@ -19991,6 +20121,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19991
20121
|
sessionBootMs,
|
|
19992
20122
|
captureStageMs,
|
|
19993
20123
|
encodeStageMs,
|
|
20124
|
+
// Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
|
|
20125
|
+
videoExtractMs,
|
|
19994
20126
|
workers: chunkWorkerCount,
|
|
19995
20127
|
captureMode,
|
|
19996
20128
|
// Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
|