@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/public-server.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
createVideoFrameInjector,
|
|
55
55
|
getEncoderPreset as getEncoderPreset3,
|
|
56
56
|
distributeFrames as distributeFrames2,
|
|
57
|
+
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
57
58
|
executeParallelCapture as executeParallelCapture2,
|
|
58
59
|
mergeWorkerFrames,
|
|
59
60
|
getSystemTotalMb,
|
|
@@ -2114,6 +2115,7 @@ function aggregateDrawElement(perfs, de) {
|
|
|
2114
2115
|
blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
|
|
2115
2116
|
blankRecaptures: drain?.blankRecaptures ?? 0,
|
|
2116
2117
|
boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
|
|
2118
|
+
riskFrames: perfs.reduce((sum, p) => sum + (p.deRiskFrames ?? 0), 0),
|
|
2117
2119
|
ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
|
|
2118
2120
|
frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
|
|
2119
2121
|
};
|
|
@@ -3750,11 +3752,21 @@ function createDrainFrameGuard(args) {
|
|
|
3750
3752
|
return buf;
|
|
3751
3753
|
};
|
|
3752
3754
|
}
|
|
3753
|
-
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
|
|
3755
|
+
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
|
|
3754
3756
|
let prev = null;
|
|
3755
3757
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
3756
3758
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
|
3757
|
-
const guardFrame = (
|
|
3759
|
+
const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
|
|
3760
|
+
const captureRisk = async (idx) => {
|
|
3761
|
+
await risk.open();
|
|
3762
|
+
lastProgressAt = Date.now();
|
|
3763
|
+
return raceAgainstStall(
|
|
3764
|
+
risk.capture(idx, frameTime(idx)),
|
|
3765
|
+
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3766
|
+
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3767
|
+
abortSignal
|
|
3768
|
+
);
|
|
3769
|
+
};
|
|
3758
3770
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
3759
3771
|
let lastProgressAt = Date.now();
|
|
3760
3772
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
@@ -3766,7 +3778,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3766
3778
|
const drainPrev = async () => {
|
|
3767
3779
|
if (!prev) return;
|
|
3768
3780
|
assertNotAborted();
|
|
3769
|
-
const buf = await guardFrame(prev
|
|
3781
|
+
const buf = await guardFrame(prev, await prev.encodeResult);
|
|
3770
3782
|
await reorderBuffer.waitForFrame(prev.idx);
|
|
3771
3783
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
|
|
3772
3784
|
reorderBuffer.advanceTo(prev.idx + 1);
|
|
@@ -3786,7 +3798,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3786
3798
|
const drainBatch = async (batch) => {
|
|
3787
3799
|
for (const item of batch) {
|
|
3788
3800
|
assertNotAborted();
|
|
3789
|
-
const buf = await guardFrame(item
|
|
3801
|
+
const buf = await guardFrame(item, await item.encodeResult);
|
|
3790
3802
|
await reorderBuffer.waitForFrame(item.idx);
|
|
3791
3803
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
|
|
3792
3804
|
reorderBuffer.advanceTo(item.idx + 1);
|
|
@@ -3804,7 +3816,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3804
3816
|
};
|
|
3805
3817
|
if (batchN > 1 && !session.onBeforeCapture) {
|
|
3806
3818
|
const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
|
|
3807
|
-
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
|
|
3819
|
+
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
|
|
3808
3820
|
let prevBatch = [];
|
|
3809
3821
|
let i = 0;
|
|
3810
3822
|
while (i < totalFrames) {
|
|
@@ -3821,6 +3833,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3821
3833
|
);
|
|
3822
3834
|
await drainBatch(prevBatch);
|
|
3823
3835
|
prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
|
|
3836
|
+
} else if (risk?.frames.has(i)) {
|
|
3837
|
+
const buffer = await captureRisk(i);
|
|
3838
|
+
await drainBatch(prevBatch);
|
|
3839
|
+
prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
|
|
3840
|
+
i++;
|
|
3824
3841
|
} else {
|
|
3825
3842
|
const { encodeResult } = await captureWithStallGuard(
|
|
3826
3843
|
i,
|
|
@@ -3836,6 +3853,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3836
3853
|
}
|
|
3837
3854
|
for (let i = 0; i < totalFrames; i++) {
|
|
3838
3855
|
assertNotAborted();
|
|
3856
|
+
if (risk?.frames.has(i)) {
|
|
3857
|
+
const buffer = await captureRisk(i);
|
|
3858
|
+
await drainPrev();
|
|
3859
|
+
prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3839
3862
|
const time = frameTime(i);
|
|
3840
3863
|
const { encodeResult } = await captureWithStallGuard(
|
|
3841
3864
|
i,
|
|
@@ -4082,6 +4105,24 @@ async function runCaptureStreamingStage(input) {
|
|
|
4082
4105
|
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4083
4106
|
probeSession = null;
|
|
4084
4107
|
}
|
|
4108
|
+
let riskSession = null;
|
|
4109
|
+
const openRiskSession = async () => {
|
|
4110
|
+
const opened = await createCaptureSession(
|
|
4111
|
+
fileServer.url,
|
|
4112
|
+
framesDir,
|
|
4113
|
+
buildCaptureOptions(),
|
|
4114
|
+
createRenderVideoFrameInjector(),
|
|
4115
|
+
{
|
|
4116
|
+
...captureCfg,
|
|
4117
|
+
useDrawElement: false,
|
|
4118
|
+
staticFrameDedup: false,
|
|
4119
|
+
enableBrowserPool: false
|
|
4120
|
+
}
|
|
4121
|
+
);
|
|
4122
|
+
riskSession = opened;
|
|
4123
|
+
await initializeSession(opened);
|
|
4124
|
+
return opened;
|
|
4125
|
+
};
|
|
4085
4126
|
try {
|
|
4086
4127
|
if (!session.isInitialized) {
|
|
4087
4128
|
await initializeSession(session);
|
|
@@ -4096,6 +4137,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4096
4137
|
blankDeterministicAccepts: 0,
|
|
4097
4138
|
blankRecaptures: 0
|
|
4098
4139
|
};
|
|
4140
|
+
const riskFrames = session.deRiskFrames;
|
|
4099
4141
|
await runWorkerEncodePipelineLoop(
|
|
4100
4142
|
session,
|
|
4101
4143
|
totalFrames,
|
|
@@ -4106,7 +4148,16 @@ async function runCaptureStreamingStage(input) {
|
|
|
4106
4148
|
onProgress,
|
|
4107
4149
|
log,
|
|
4108
4150
|
deDrainStats,
|
|
4109
|
-
abortSignal
|
|
4151
|
+
abortSignal,
|
|
4152
|
+
riskFrames && riskFrames.size > 0 ? {
|
|
4153
|
+
frames: riskFrames,
|
|
4154
|
+
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4155
|
+
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4156
|
+
open: async () => {
|
|
4157
|
+
riskSession ??= await openRiskSession();
|
|
4158
|
+
},
|
|
4159
|
+
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4160
|
+
} : void 0
|
|
4110
4161
|
);
|
|
4111
4162
|
} else {
|
|
4112
4163
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
@@ -4144,6 +4195,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4144
4195
|
} finally {
|
|
4145
4196
|
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4146
4197
|
await closeCaptureSession(session);
|
|
4198
|
+
if (riskSession) await closeCaptureSession(riskSession);
|
|
4147
4199
|
}
|
|
4148
4200
|
}
|
|
4149
4201
|
const encodeFrom = job.progress;
|
|
@@ -4303,7 +4355,7 @@ function openSegmentedSessionReuse(input, session) {
|
|
|
4303
4355
|
async function openSegmentedSession(input) {
|
|
4304
4356
|
const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
|
|
4305
4357
|
const videoInjector = input.createRenderVideoFrameInjector();
|
|
4306
|
-
const captureOptions = input.buildCaptureOptions();
|
|
4358
|
+
const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
|
|
4307
4359
|
const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
|
|
4308
4360
|
parallel: (input.workerCount ?? 1) > 1
|
|
4309
4361
|
});
|
|
@@ -5548,6 +5600,7 @@ import {
|
|
|
5548
5600
|
parseVideoElements,
|
|
5549
5601
|
parseImageElements,
|
|
5550
5602
|
parseAudioElements,
|
|
5603
|
+
isSelfOrAncestorHidden,
|
|
5551
5604
|
resolveReferencedStart
|
|
5552
5605
|
} from "@hyperframes/engine";
|
|
5553
5606
|
function resolveHostEnd(host, hostStart) {
|
|
@@ -5608,6 +5661,15 @@ function collectHostWindows(html) {
|
|
|
5608
5661
|
}
|
|
5609
5662
|
return windows;
|
|
5610
5663
|
}
|
|
5664
|
+
function collectHiddenVideoIds(html) {
|
|
5665
|
+
const { document: document2 } = parseHTML2(html);
|
|
5666
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5667
|
+
for (const element of document2.querySelectorAll("video")) {
|
|
5668
|
+
const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
|
|
5669
|
+
if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
|
|
5670
|
+
}
|
|
5671
|
+
return hidden;
|
|
5672
|
+
}
|
|
5611
5673
|
function toAbsoluteWindow(start, end, window2) {
|
|
5612
5674
|
const absoluteStart = resolveAbsoluteMediaStartSeconds({
|
|
5613
5675
|
authoredStart: start,
|
|
@@ -5641,8 +5703,13 @@ function collectRenderMedia(html) {
|
|
|
5641
5703
|
const windows = collectHostWindows(html);
|
|
5642
5704
|
const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
|
|
5643
5705
|
const droppedMediaIds = /* @__PURE__ */ new Set();
|
|
5706
|
+
const hiddenIds = collectHiddenVideoIds(html);
|
|
5644
5707
|
const videos = [];
|
|
5645
5708
|
for (const video of parseVideoElements(html)) {
|
|
5709
|
+
if (hiddenIds.has(video.id)) {
|
|
5710
|
+
droppedMediaIds.add(video.id);
|
|
5711
|
+
continue;
|
|
5712
|
+
}
|
|
5646
5713
|
const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
|
|
5647
5714
|
if (!absolute) {
|
|
5648
5715
|
droppedMediaIds.add(video.id);
|
|
@@ -5852,7 +5919,7 @@ async function runAnimatedGifTranscode(request) {
|
|
|
5852
5919
|
failureReason: result.failureReason
|
|
5853
5920
|
});
|
|
5854
5921
|
}
|
|
5855
|
-
if (result.terminationReason === "
|
|
5922
|
+
if (result.terminationReason === "inactivity") {
|
|
5856
5923
|
throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
|
|
5857
5924
|
}
|
|
5858
5925
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
@@ -6319,10 +6386,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
|
|
|
6319
6386
|
function detectThreeDTransformUsage(html) {
|
|
6320
6387
|
return THREE_D_CONTEXT_PATTERN.test(html);
|
|
6321
6388
|
}
|
|
6322
|
-
var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
|
|
6323
|
-
function detectMixBlendModeUsage(html) {
|
|
6324
|
-
return MIX_BLEND_MODE_PATTERN.test(html);
|
|
6325
|
-
}
|
|
6326
6389
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
6327
6390
|
function detectAncestorBackgroundImage(html) {
|
|
6328
6391
|
const { document: document2 } = parseHTML4(html);
|
|
@@ -6475,8 +6538,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6475
6538
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
6476
6539
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
6477
6540
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
6541
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
6478
6542
|
const clampResults = await Promise.all(
|
|
6479
|
-
preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
|
|
6543
|
+
preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
|
|
6480
6544
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
6481
6545
|
el.src,
|
|
6482
6546
|
el.mediaStart,
|
|
@@ -7335,7 +7399,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7335
7399
|
const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
|
|
7336
7400
|
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
7337
7401
|
const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
|
|
7338
|
-
const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
|
|
7339
7402
|
const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
|
|
7340
7403
|
const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
|
|
7341
7404
|
injectTextRenderingRule(
|
|
@@ -7449,7 +7512,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7449
7512
|
renderModeHints,
|
|
7450
7513
|
hasShaderTransitions,
|
|
7451
7514
|
usesThreeDTransforms,
|
|
7452
|
-
usesMixBlendMode,
|
|
7453
7515
|
hasAncestorBackgroundImage
|
|
7454
7516
|
};
|
|
7455
7517
|
}
|
|
@@ -7856,13 +7918,6 @@ async function runCompileStage(input) {
|
|
|
7856
7918
|
"[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."
|
|
7857
7919
|
);
|
|
7858
7920
|
}
|
|
7859
|
-
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
7860
|
-
cfg.useDrawElement = false;
|
|
7861
|
-
deCompileGate = "mix_blend_mode";
|
|
7862
|
-
log.info(
|
|
7863
|
-
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7864
|
-
);
|
|
7865
|
-
}
|
|
7866
7921
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
7867
7922
|
cfg.useDrawElement = false;
|
|
7868
7923
|
deCompileGate = "ancestor_background_image";
|
|
@@ -8156,6 +8211,7 @@ async function runProbeStage(input) {
|
|
|
8156
8211
|
height,
|
|
8157
8212
|
fps: job.config.fps,
|
|
8158
8213
|
format: captureFormat,
|
|
8214
|
+
transparent: needsAlpha,
|
|
8159
8215
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
8160
8216
|
variables: job.config.variables,
|
|
8161
8217
|
deviceScaleFactor,
|
|
@@ -9257,10 +9313,11 @@ async function constrainAacTruePeak(input) {
|
|
|
9257
9313
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
9258
9314
|
return enforceAacTruePeak({
|
|
9259
9315
|
audioPath: input.audioPath,
|
|
9316
|
+
sourcePath: input.sourcePath,
|
|
9260
9317
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
9261
9318
|
signal: input.signal,
|
|
9262
|
-
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
9263
|
-
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9319
|
+
probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
|
|
9320
|
+
encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9264
9321
|
});
|
|
9265
9322
|
}
|
|
9266
9323
|
async function enforceAacTruePeak(input) {
|
|
@@ -9270,6 +9327,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9270
9327
|
const correctedPath = join18(scratchDir, "audio.m4a");
|
|
9271
9328
|
const measurements = [];
|
|
9272
9329
|
let attenuationDb = 0;
|
|
9330
|
+
let sourcePeakDbfs;
|
|
9273
9331
|
let measuredPath = input.audioPath;
|
|
9274
9332
|
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
9275
9333
|
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
@@ -9286,7 +9344,11 @@ async function enforceAacTruePeak(input) {
|
|
|
9286
9344
|
}
|
|
9287
9345
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
9288
9346
|
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
9289
|
-
|
|
9347
|
+
if (input.sourcePath && sourcePeakDbfs === void 0) {
|
|
9348
|
+
sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
|
|
9349
|
+
}
|
|
9350
|
+
const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
|
|
9351
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
|
|
9290
9352
|
const result = await input.encode(
|
|
9291
9353
|
input.audioPath,
|
|
9292
9354
|
correctedPath,
|
|
@@ -9722,7 +9784,7 @@ async function runCaptureStage(input) {
|
|
|
9722
9784
|
);
|
|
9723
9785
|
}
|
|
9724
9786
|
}
|
|
9725
|
-
const captureOptions = buildCaptureOptions();
|
|
9787
|
+
const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
|
|
9726
9788
|
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
9727
9789
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
9728
9790
|
framesDir,
|
|
@@ -9743,6 +9805,7 @@ async function runCaptureStage(input) {
|
|
|
9743
9805
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
9744
9806
|
abortSignal,
|
|
9745
9807
|
frameRangeStart: frameRange?.startFrame,
|
|
9808
|
+
interleaveFrames: input.interleaveFrames,
|
|
9746
9809
|
dedupPerfs,
|
|
9747
9810
|
pageErrorLines: workerPageErrors,
|
|
9748
9811
|
onProgress: (progress) => {
|
|
@@ -11258,9 +11321,6 @@ async function runEncodeStage(input) {
|
|
|
11258
11321
|
return { encodeMs: Date.now() - stage5Start };
|
|
11259
11322
|
}
|
|
11260
11323
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
11261
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
11262
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
11263
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
11264
11324
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
11265
11325
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
11266
11326
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -11271,7 +11331,7 @@ async function runEncodeStage(input) {
|
|
|
11271
11331
|
encoderOpts,
|
|
11272
11332
|
chunkedEncodeSize,
|
|
11273
11333
|
abortSignal,
|
|
11274
|
-
|
|
11334
|
+
engineCfg,
|
|
11275
11335
|
onFramesEncoded
|
|
11276
11336
|
) : await encodeFramesFromDir(
|
|
11277
11337
|
framesDir,
|
|
@@ -11279,7 +11339,7 @@ async function runEncodeStage(input) {
|
|
|
11279
11339
|
videoOnlyPath,
|
|
11280
11340
|
encoderOpts,
|
|
11281
11341
|
abortSignal,
|
|
11282
|
-
|
|
11342
|
+
engineCfg,
|
|
11283
11343
|
onFramesEncoded
|
|
11284
11344
|
);
|
|
11285
11345
|
assertNotAborted();
|
|
@@ -11621,16 +11681,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11621
11681
|
}
|
|
11622
11682
|
return ranges;
|
|
11623
11683
|
}
|
|
11624
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
11684
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11625
11685
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11626
11686
|
const batches = [];
|
|
11687
|
+
const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11688
|
+
if (stride > 1) {
|
|
11689
|
+
const spans = /* @__PURE__ */ new Map();
|
|
11690
|
+
for (const range of ranges) {
|
|
11691
|
+
for (let frame = range.startFrame; frame < range.endFrame; frame++) {
|
|
11692
|
+
const span = spans.get(frame % stride);
|
|
11693
|
+
if (span) span.last = frame;
|
|
11694
|
+
else spans.set(frame % stride, { first: frame, last: frame });
|
|
11695
|
+
}
|
|
11696
|
+
}
|
|
11697
|
+
const strided = [...spans.values()];
|
|
11698
|
+
for (let i = 0; i < strided.length; i += workersPerBatch) {
|
|
11699
|
+
const batchIndex = batches.length;
|
|
11700
|
+
batches.push(
|
|
11701
|
+
strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
|
|
11702
|
+
workerId,
|
|
11703
|
+
startFrame: rangeStart + first,
|
|
11704
|
+
endFrame: rangeStart + last + 1,
|
|
11705
|
+
frameStride: stride,
|
|
11706
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11707
|
+
outputFrameOffset: rangeStart
|
|
11708
|
+
}))
|
|
11709
|
+
);
|
|
11710
|
+
}
|
|
11711
|
+
return batches;
|
|
11712
|
+
}
|
|
11627
11713
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
11628
11714
|
const batchIndex = batches.length;
|
|
11629
11715
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
11630
11716
|
workerId,
|
|
11631
11717
|
startFrame: rangeStart + range.startFrame,
|
|
11632
11718
|
endFrame: rangeStart + range.endFrame,
|
|
11633
|
-
outputDir:
|
|
11719
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11634
11720
|
outputFrameOffset: rangeStart
|
|
11635
11721
|
}));
|
|
11636
11722
|
batches.push(batch);
|
|
@@ -11723,8 +11809,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11723
11809
|
currentWorkers,
|
|
11724
11810
|
attemptWorkDir,
|
|
11725
11811
|
attempt,
|
|
11726
|
-
rangeStart
|
|
11727
|
-
|
|
11812
|
+
rangeStart,
|
|
11813
|
+
options.interleaveFrames ? options.initialWorkerCount : 1
|
|
11814
|
+
) : [
|
|
11815
|
+
(options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
|
|
11816
|
+
options.totalFrames,
|
|
11817
|
+
currentWorkers,
|
|
11818
|
+
attemptWorkDir,
|
|
11819
|
+
rangeStart
|
|
11820
|
+
)
|
|
11821
|
+
];
|
|
11728
11822
|
options.dedupPerfs.length = 0;
|
|
11729
11823
|
try {
|
|
11730
11824
|
for (const tasks of batches) {
|
|
@@ -12176,6 +12270,9 @@ function resolveDeShortBand(args) {
|
|
|
12176
12270
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
12177
12271
|
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;
|
|
12178
12272
|
}
|
|
12273
|
+
function probeNeedsScreenshotFrames(session) {
|
|
12274
|
+
return (session?.deRiskFrames?.size ?? 0) > 0;
|
|
12275
|
+
}
|
|
12179
12276
|
function isProbeDeGated(session) {
|
|
12180
12277
|
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
12181
12278
|
}
|
|
@@ -12833,6 +12930,7 @@ async function executeRenderPipeline(input) {
|
|
|
12833
12930
|
height,
|
|
12834
12931
|
fps: job.config.fps,
|
|
12835
12932
|
format: captureImageFormat,
|
|
12933
|
+
transparent: needsAlpha,
|
|
12836
12934
|
quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
12837
12935
|
variables: job.config.variables,
|
|
12838
12936
|
deviceScaleFactor,
|
|
@@ -12854,6 +12952,7 @@ async function executeRenderPipeline(input) {
|
|
|
12854
12952
|
// Probe-resolved duration: drawElement self-verification derives its
|
|
12855
12953
|
// sample frame indices from this so they land inside the drained range.
|
|
12856
12954
|
compositionDurationSeconds: job.duration,
|
|
12955
|
+
deFrameFallback: true,
|
|
12857
12956
|
requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
|
|
12858
12957
|
// Live route for Chrome memory (spec §5 Phase −1). The aggregate route
|
|
12859
12958
|
// through CapturePerfSummary only exists on success; a mid-capture
|
|
@@ -12954,7 +13053,7 @@ async function executeRenderPipeline(input) {
|
|
|
12954
13053
|
minFrames: deParallelMinFrames,
|
|
12955
13054
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12956
13055
|
supersampling: deviceScaleFactor !== 1,
|
|
12957
|
-
probeDeGated: isProbeDeGated(probeSession),
|
|
13056
|
+
probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
|
|
12958
13057
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12959
13058
|
routerEnabled: deParallelRouterEnabled,
|
|
12960
13059
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -12996,6 +13095,8 @@ async function executeRenderPipeline(input) {
|
|
|
12996
13095
|
deInversionEligible = false;
|
|
12997
13096
|
deParallelRouterEligible = false;
|
|
12998
13097
|
deShortBand = void 0;
|
|
13098
|
+
} else if (probeNeedsScreenshotFrames(probeSession)) {
|
|
13099
|
+
deParallelRouterEligible = false;
|
|
12999
13100
|
}
|
|
13000
13101
|
}
|
|
13001
13102
|
const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
|
|
@@ -13187,7 +13288,8 @@ async function executeRenderPipeline(input) {
|
|
|
13187
13288
|
deParallelStreamForced
|
|
13188
13289
|
);
|
|
13189
13290
|
const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
|
|
13190
|
-
|
|
13291
|
+
const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
|
|
13292
|
+
if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
|
|
13191
13293
|
useDrawElement: cfg.useDrawElement,
|
|
13192
13294
|
fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
|
|
13193
13295
|
useStreamingEncode,
|
|
@@ -13861,7 +13963,8 @@ async function executeRenderPipeline(input) {
|
|
|
13861
13963
|
abortSignal: executionSignal,
|
|
13862
13964
|
assertNotAborted,
|
|
13863
13965
|
onProgress,
|
|
13864
|
-
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13966
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13967
|
+
interleaveFrames: overlap !== null
|
|
13865
13968
|
})
|
|
13866
13969
|
);
|
|
13867
13970
|
let captureRes;
|
|
@@ -105,6 +105,54 @@ type CliOptions = {
|
|
|
105
105
|
*/
|
|
106
106
|
mode: HarnessMode;
|
|
107
107
|
};
|
|
108
|
+
type TestResult = {
|
|
109
|
+
suite: TestSuite;
|
|
110
|
+
passed: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Set when `--mode=distributed-simulated` skips a fixture that the
|
|
113
|
+
* distributed pipeline can't run (HDR, NTSC fps, fps outside DISTRIBUTED_FPS).
|
|
114
|
+
* `passed` is `true` for skipped fixtures — skipping is a clean outcome,
|
|
115
|
+
* not a failure — but the summary distinguishes them.
|
|
116
|
+
*/
|
|
117
|
+
skipped?: {
|
|
118
|
+
reason: string;
|
|
119
|
+
};
|
|
120
|
+
compilation?: {
|
|
121
|
+
passed: boolean;
|
|
122
|
+
errors: string[];
|
|
123
|
+
warnings: string[];
|
|
124
|
+
};
|
|
125
|
+
visual?: {
|
|
126
|
+
passed: boolean;
|
|
127
|
+
failedFrames: number;
|
|
128
|
+
checkpoints: Array<{
|
|
129
|
+
time: number;
|
|
130
|
+
psnr: number;
|
|
131
|
+
passed: boolean;
|
|
132
|
+
}>;
|
|
133
|
+
};
|
|
134
|
+
audio?: {
|
|
135
|
+
passed: boolean;
|
|
136
|
+
correlation: number;
|
|
137
|
+
lagWindows: number;
|
|
138
|
+
/**
|
|
139
|
+
* Residual Overall RMS (dBFS) of `rendered - snapshot`. Present only
|
|
140
|
+
* when the fixture opts in via `meta.maxAudioResidualRmsDb`.
|
|
141
|
+
* `Number.NEGATIVE_INFINITY` ⇒ perfect cancellation. `NaN` ⇒ residual
|
|
142
|
+
* check could not run (missing ffmpeg, duration mismatch, ...); see
|
|
143
|
+
* `audio.residualError` for the reason.
|
|
144
|
+
*/
|
|
145
|
+
residualRmsDb?: number;
|
|
146
|
+
residualError?: string;
|
|
147
|
+
};
|
|
148
|
+
streamDurationParity?: {
|
|
149
|
+
passed: boolean;
|
|
150
|
+
videoDurationSeconds: number;
|
|
151
|
+
audioDurationSeconds: number;
|
|
152
|
+
driftSeconds: number;
|
|
153
|
+
};
|
|
154
|
+
renderedOutputPath?: string;
|
|
155
|
+
};
|
|
108
156
|
export declare function parseArgs(argv: string[]): CliOptions;
|
|
109
157
|
export declare function discoverTestSuites(testsDir: string, filterNames: string[], excludeTags?: string[]): TestSuite[];
|
|
110
158
|
/** The frame a checkpoint time samples. Shared so selection and lookup agree. */
|
|
@@ -154,4 +202,9 @@ export type StreamDurationParity = {
|
|
|
154
202
|
};
|
|
155
203
|
export declare function checkStreamDurationParity(videoPath: string): Promise<StreamDurationParity | null>;
|
|
156
204
|
export declare function createRegressionTempRoot(suiteId: string, parent?: string): string;
|
|
205
|
+
export declare function runTestSuite(suite: TestSuite, options: {
|
|
206
|
+
update: boolean;
|
|
207
|
+
keepTemp: boolean;
|
|
208
|
+
mode: HarnessMode;
|
|
209
|
+
}): Promise<TestResult>;
|
|
157
210
|
export {};
|
|
@@ -9,6 +9,8 @@ export declare function planFrameWeights(input: {
|
|
|
9
9
|
width: number;
|
|
10
10
|
height: number;
|
|
11
11
|
}): Float64Array;
|
|
12
|
+
export declare function withChunkCaptureCost(slices: ChunkSliceJson[], weights: Float64Array): ChunkSliceJson[];
|
|
13
|
+
export declare function effectiveChunkFrames(frames: number, captureCost?: number): number;
|
|
12
14
|
/**
|
|
13
15
|
* Cut `[0, weights.length)` into `chunkCount` ranges of ~equal summed weight, each within
|
|
14
16
|
* `[minFrames, maxFrames]` where the frame count allows. `null` when the cost is uniform.
|
|
@@ -20,8 +20,6 @@ export interface CompiledComposition {
|
|
|
20
20
|
hasShaderTransitions: boolean;
|
|
21
21
|
/** Author HTML/CSS/scripts use a CSS 3D rendering context (pre-CDN-inline scan). */
|
|
22
22
|
usesThreeDTransforms: boolean;
|
|
23
|
-
/** Author HTML/CSS use mix-blend-mode (pre-CDN-inline scan). */
|
|
24
|
-
usesMixBlendMode: boolean;
|
|
25
23
|
/** Ancestors of the composition root carry a background-image (gradient/url). */
|
|
26
24
|
hasAncestorBackgroundImage: boolean;
|
|
27
25
|
}
|
|
@@ -124,6 +124,8 @@ export declare function constrainAacTruePeak(input: {
|
|
|
124
124
|
targetDurationSeconds: number;
|
|
125
125
|
signal?: AbortSignal;
|
|
126
126
|
timeoutMs?: number;
|
|
127
|
+
probeTruePeakDbfs?: (audioPath: string, signal?: AbortSignal) => Promise<number>;
|
|
128
|
+
encodeAac?: AacEncoder;
|
|
127
129
|
}): Promise<{
|
|
128
130
|
success: boolean;
|
|
129
131
|
error?: string;
|
|
@@ -101,7 +101,7 @@ export interface CompileStageResult {
|
|
|
101
101
|
*/
|
|
102
102
|
forceScreenshot: boolean;
|
|
103
103
|
/** Low-cardinality compile-time gate that disabled default drawElement:
|
|
104
|
-
* `3d` | `
|
|
104
|
+
* `3d` | `ancestor_background_image` | `shader_transitions`. Undefined when none fired. */
|
|
105
105
|
deCompileGate?: string;
|
|
106
106
|
}
|
|
107
107
|
export declare function runCompileStage(input: CompileStageInput): Promise<CompileStageResult>;
|
|
@@ -87,6 +87,7 @@ export interface ChunkSliceJson {
|
|
|
87
87
|
startFrame: number;
|
|
88
88
|
/** Exclusive upper bound — chunk workers iterate frames in `[startFrame, endFrame)`. */
|
|
89
89
|
endFrame: number;
|
|
90
|
+
captureCost?: number;
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
93
|
* Inputs to `freezePlan`. `planDir` already contains `compiled/`,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* and placed by the composition hosts it sits inside.
|
|
7
7
|
*/
|
|
8
8
|
import { type VideoElement, type ImageElement, type AudioElement } from "@hyperframes/engine";
|
|
9
|
+
/** Ids, as the parsers report them, of videos that are `data-hidden` or sit inside such an element. */
|
|
10
|
+
export declare function collectHiddenVideoIds(html: string): Set<string>;
|
|
9
11
|
export interface RenderMedia {
|
|
10
12
|
videos: VideoElement[];
|
|
11
13
|
audios: AudioElement[];
|