@hyperframes/engine 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/dist/config.d.ts +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/services/audioMixer.js +3 -9
- package/dist/services/browserManager.d.ts +3 -3
- package/dist/services/browserManager.js +37 -17
- package/dist/services/chunkEncoder.js +6 -12
- package/dist/services/frameCapture.d.ts +11 -1
- package/dist/services/frameCapture.js +186 -62
- package/dist/services/parallelCoordinator.js +12 -10
- package/dist/services/riskFrames.d.ts +30 -0
- package/dist/services/riskFrames.js +101 -0
- package/dist/services/screenshotService.d.ts +7 -0
- package/dist/services/screenshotService.js +4 -0
- package/dist/services/systemMemory.d.ts +2 -0
- package/dist/services/systemMemory.js +38 -1
- package/dist/services/threeDProjection.d.ts +14 -0
- package/dist/services/threeDProjection.js +117 -41
- package/dist/services/videoFrameExtractor.js +9 -5
- package/dist/services/videoFrameInjector.js +2 -1
- package/dist/types.d.ts +6 -2
- package/dist/utils/psnrFilterAvailability.d.ts +1 -0
- package/dist/utils/psnrFilterAvailability.js +17 -10
- package/dist/utils/runFfmpeg.d.ts +2 -3
- package/dist/utils/runFfmpeg.js +13 -16
- package/dist/utils/urlDownloader.js +21 -7
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
|
-
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, } from "@hyperframes/core";
|
|
12
|
+
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, SCENE_CALLBACK_ERROR_PREFIX, } from "@hyperframes/core";
|
|
13
13
|
import { holdRenderFrameVideos } from "./renderFrameVideoHold.js";
|
|
14
14
|
import { DrawElementCaptureError } from "./drawElementCaptureError.js";
|
|
15
15
|
import { encodePng } from "../utils/alphaBlit.js";
|
|
@@ -17,9 +17,11 @@ import { DEFAULT_SAMPLES_PER_FRAME, MotionBlurAccumulator, SPATIAL_TWEEN_PROPERT
|
|
|
17
17
|
import { describePageMotionBlur, describePageMotionBlurAtEnd, installPageMotionBlur, } from "./pageMotionBlur.js";
|
|
18
18
|
// ── Extracted modules ───────────────────────────────────────────────────────
|
|
19
19
|
import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, captureWithoutWaylandDisplay, resolveHeadlessShellPath, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, } from "./browserManager.js";
|
|
20
|
-
import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
|
|
20
|
+
import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, capturesAlpha, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
|
|
21
21
|
import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, DE_CANVAS_NOT_INITIALIZED_CODE, } from "./drawElementService.js";
|
|
22
|
-
import { initThreeDProjection,
|
|
22
|
+
import { initThreeDProjection, scanCssEffects } from "./threeDProjection.js";
|
|
23
|
+
import { planRiskFrames, scanCssEffectLiveFrames, riskSessionFits, withFramesAfterCallback, MAX_RISK_FRAME_FRACTION, } from "./riskFrames.js";
|
|
24
|
+
import { getAvailableMemoryMb } from "./systemMemory.js";
|
|
23
25
|
import { isPsnrFilterAvailable } from "../utils/psnrFilterAvailability.js";
|
|
24
26
|
import { DEFAULT_CONFIG, applyConcreteGpuScreenshotClamp } from "../config.js";
|
|
25
27
|
import { cloneCaptureWarnings } from "./captureWarning.js";
|
|
@@ -472,20 +474,20 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
472
474
|
if ((session.config?.useDrawElement ?? false) && supersampling) {
|
|
473
475
|
session.deGateReason = "supersampling";
|
|
474
476
|
session.deFallbackTrigger = "supersampling";
|
|
475
|
-
console.
|
|
477
|
+
console.error("[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
|
|
476
478
|
"ignores deviceScaleFactor, so scaled (DPR other than 1) output uses screenshot capture.");
|
|
477
479
|
}
|
|
478
480
|
if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
|
|
479
481
|
session.deGateReason = "render_mode_hint";
|
|
480
482
|
session.deFallbackTrigger = "render_mode_hint";
|
|
481
|
-
console.
|
|
483
|
+
console.error("[engine] fast capture falls back to screenshot: render-mode compatibility " +
|
|
482
484
|
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).");
|
|
483
485
|
}
|
|
484
486
|
if (useDrawElement) {
|
|
485
487
|
const gpuBackend = await detectGpuBackend(page);
|
|
486
488
|
session.isSwiftShader = gpuBackend.isSwiftShader;
|
|
487
489
|
session.gpuRenderer = classifyGpuRenderer(gpuBackend.renderer);
|
|
488
|
-
const transparent = session.options
|
|
490
|
+
const transparent = capturesAlpha(session.options);
|
|
489
491
|
async function routeToFallback() {
|
|
490
492
|
session.captureMode = session.launchCaptureMode;
|
|
491
493
|
if (transparent) {
|
|
@@ -526,7 +528,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
526
528
|
if (!supportsDrawElement) {
|
|
527
529
|
session.deGateReason = "unsupported_chrome";
|
|
528
530
|
session.deFallbackTrigger = "unsupported_chrome";
|
|
529
|
-
console.
|
|
531
|
+
console.error(`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
|
530
532
|
"this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only " +
|
|
531
533
|
"feature); run `hyperframes browser ensure --force` to fetch a supported " +
|
|
532
534
|
"build, or set HYPERFRAMES_BROWSER_PATH to one.");
|
|
@@ -574,7 +576,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
574
576
|
// "screenshot": on a BeginFrame-launched browser (Linux fast capture)
|
|
575
577
|
// Page.captureScreenshot hangs for the full protocol timeout, while
|
|
576
578
|
// beginFrameCapture is the platform's normal baseline path.
|
|
577
|
-
console.
|
|
579
|
+
console.error(`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
|
578
580
|
"SwiftShader (software rasterizer, no GPU egress to skip, drawElement is " +
|
|
579
581
|
"parity-or-slower; see fast-capture-limitations.md)");
|
|
580
582
|
await routeToFallback();
|
|
@@ -585,18 +587,37 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
585
587
|
// path — drawElementImage can't reproduce either, producing 18–49 dB
|
|
586
588
|
// damaged frames (community eval). Fall back to the platform baseline.
|
|
587
589
|
// HF_FAST_CAPTURE_CSSFX=true bypasses for R&D.
|
|
590
|
+
// Frames this render captures by screenshot instead (see CaptureSession.deRiskFrames);
|
|
591
|
+
// only a session whose caller can hand them over (worker-encode pipeline) may keep drawElement.
|
|
592
|
+
const perFrame = session.options.deFrameFallback === true &&
|
|
593
|
+
drawElementWorkerEncodeEligible(session, transparent) &&
|
|
594
|
+
!forceDE;
|
|
595
|
+
const riskFrames = new Set();
|
|
596
|
+
const riskReasons = [];
|
|
597
|
+
let cssFrames = 0;
|
|
598
|
+
const routeWholeRender = async (reason, trigger, message) => {
|
|
599
|
+
session.deGateReason = reason;
|
|
600
|
+
session.deFallbackTrigger = trigger;
|
|
601
|
+
console.error(`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ${message}`);
|
|
602
|
+
await routeToFallback();
|
|
603
|
+
};
|
|
588
604
|
if (!forceDE && process.env.HF_FAST_CAPTURE_CSSFX !== "true") {
|
|
589
|
-
const cssFx = await
|
|
605
|
+
const cssFx = (await scanCssEffects(page, false)).reason;
|
|
590
606
|
if (cssFx) {
|
|
591
|
-
|
|
592
|
-
// Full specific effect from `detectCssEffectRisk` (e.g. "filter:blur",
|
|
607
|
+
// Full specific effect from `scanCssEffects` (e.g. "filter:blur",
|
|
593
608
|
// "mix-blend-mode", "webgl-context") — `deGateReason` keeps its
|
|
594
609
|
// low-cardinality prefix, `deFallbackTrigger` the full value.
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
await
|
|
599
|
-
|
|
610
|
+
const gateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
|
|
611
|
+
const gateMessage = `${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`;
|
|
612
|
+
cssFrames = perFrame ? await renderFrameCount(session, page) : 0;
|
|
613
|
+
const live = cssFrames > 0 ? await cssEffectRiskFrames(session, cssFrames) : null;
|
|
614
|
+
if (!live) {
|
|
615
|
+
await routeWholeRender(gateReason, cssFx, gateMessage);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
for (const f of live)
|
|
619
|
+
riskFrames.add(f);
|
|
620
|
+
riskReasons.push(cssFx);
|
|
600
621
|
}
|
|
601
622
|
}
|
|
602
623
|
// Lim 7: timeline-interval at-risk predictor. Walk window.__timelines and
|
|
@@ -622,15 +643,36 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
622
643
|
const fractionFloor = Number(process.env.HF_FAST_CAPTURE_INTERVAL_FRACTION ?? "0");
|
|
623
644
|
logInitPhase(`timeline at-risk predictor: ${atRisk.size}/${totalFrames} frames (${Math.round(atRiskFraction * 100)}%)`);
|
|
624
645
|
if (atRisk.size > 0 && atRiskFraction > fractionFloor) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
646
|
+
if (perFrame) {
|
|
647
|
+
for (const f of atRisk)
|
|
648
|
+
riskFrames.add(f);
|
|
649
|
+
riskReasons.push("at_risk_timeline");
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
await routeWholeRender("at_risk_timeline", "at_risk_timeline", `${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop ` +
|
|
653
|
+
`(blend/3D/clip/mask); drawElementImage drops these mid-animation ` +
|
|
654
|
+
`(deterministic timeline gate; see fast-capture-limitations.md Lim 7)`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (riskFrames.size > 0) {
|
|
660
|
+
const total = cssFrames > 0 ? cssFrames : await renderFrameCount(session, page);
|
|
661
|
+
const plan = total > 0 ? planRiskFrames(riskFrames, total) : null;
|
|
662
|
+
if (!plan) {
|
|
663
|
+
await routeWholeRender("risk_frames", riskReasons.join("+"), `${riskFrames.size}/${total} frames need screenshot capture (${riskReasons.join(", ")}), ` +
|
|
664
|
+
`too many to keep fast capture for the rest`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!riskSessionFits(getAvailableMemoryMb(), session.options.width * session.options.height)) {
|
|
668
|
+
await routeWholeRender("risk_session_memory", riskReasons.join("+"), `${plan.size} frames need screenshot capture (${riskReasons.join(", ")}) but too little ` +
|
|
669
|
+
`memory is available for per-frame capture`);
|
|
632
670
|
return;
|
|
633
671
|
}
|
|
672
|
+
session.deRiskFrames = plan;
|
|
673
|
+
session.deRiskReason = riskReasons.join("+");
|
|
674
|
+
logInitPhase(`fast capture keeps ${total - plan.size}/${total} frames; ${plan.size} frame(s) ` +
|
|
675
|
+
`capture by screenshot (${session.deRiskReason})`);
|
|
634
676
|
}
|
|
635
677
|
// Rewrite CSS 3D contexts into WebGL-projected canvases BEFORE the
|
|
636
678
|
// layoutsubtree canvas goes in (rects are measured in normal layout).
|
|
@@ -640,7 +682,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
640
682
|
if (!forceDE && !threeD.ok) {
|
|
641
683
|
session.deGateReason = "3d_init_failed";
|
|
642
684
|
session.deFallbackTrigger = "3d_init_failed";
|
|
643
|
-
console.
|
|
685
|
+
console.error(`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
|
644
686
|
`3D projection init failed (${threeD.reason ?? "unknown"})`);
|
|
645
687
|
await routeToFallback();
|
|
646
688
|
return;
|
|
@@ -653,6 +695,10 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
653
695
|
// passed) and the DOM is still normal (canvas not yet injected, so the
|
|
654
696
|
// verification screenshots are valid). drawElement-path only; see armStaticDedup.
|
|
655
697
|
await armStaticDedup(session, page, logInitPhase);
|
|
698
|
+
// A static frame reuses the previous drawElement capture; a risk frame is captured by
|
|
699
|
+
// another session, so its neighbours must not reuse across it.
|
|
700
|
+
for (const f of session.deRiskFrames ?? [])
|
|
701
|
+
session.staticFrames?.delete(f);
|
|
656
702
|
// Video comps on injector-less sessions (probe sessions initialize before
|
|
657
703
|
// video extraction) DEFER the rest of the drawElement init: ground-truth
|
|
658
704
|
// screenshots would capture black <video> boxes, and once the canvas is
|
|
@@ -669,10 +715,53 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
669
715
|
await finalizeDrawElementInit(session, page, logInitPhase, { transparent, forceDE });
|
|
670
716
|
}
|
|
671
717
|
}
|
|
672
|
-
else if (session.options
|
|
718
|
+
else if (capturesAlpha(session.options)) {
|
|
673
719
|
await initTransparentBackground(session.page);
|
|
674
720
|
}
|
|
675
721
|
}
|
|
722
|
+
/** Worker-encode pipeline: macOS hardware GPU path only, JPEG output only. */
|
|
723
|
+
function drawElementWorkerEncodeEligible(session, transparent) {
|
|
724
|
+
return ((session.config?.enableDrawElementWorkerEncode ?? false) &&
|
|
725
|
+
!transparent &&
|
|
726
|
+
session.beginFrameTimeTicks === 0);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The frames a CSS effect shows on, or null when it cannot be bounded. Scans in a throwaway
|
|
730
|
+
* session: the scan seeks the timeline end to end, which this page's render must not inherit.
|
|
731
|
+
*/
|
|
732
|
+
async function cssEffectRiskFrames(session, totalFrames) {
|
|
733
|
+
let scratch = null;
|
|
734
|
+
try {
|
|
735
|
+
scratch = await createCaptureSession(session.serverUrl, session.outputDir, { ...session.options, onMemorySample: undefined }, null, {
|
|
736
|
+
...session.config,
|
|
737
|
+
useDrawElement: false,
|
|
738
|
+
staticFrameDedup: false,
|
|
739
|
+
enableBrowserPool: false,
|
|
740
|
+
});
|
|
741
|
+
await initializeSession(scratch);
|
|
742
|
+
const scan = await scanCssEffects(scratch.page, true);
|
|
743
|
+
if (!scan.reason || !scan.bounded)
|
|
744
|
+
return null;
|
|
745
|
+
const fps = fpsToNumber(session.options.fps);
|
|
746
|
+
const live = await scanCssEffectLiveFrames(scratch.page, fps, totalFrames, totalFrames * MAX_RISK_FRAME_FRACTION);
|
|
747
|
+
return live && withFramesAfterCallback(live, scan.callbackFrom, fps, totalFrames);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
console.error(`[engine] fast capture: effect scan failed (${error instanceof Error ? error.message : String(error)})`);
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
if (scratch)
|
|
755
|
+
await closeCaptureSession(scratch).catch(() => { });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/** Frames the render will capture, from the producer's duration when it set one. */
|
|
759
|
+
async function renderFrameCount(session, page) {
|
|
760
|
+
const duration = session.options.compositionDurationSeconds ??
|
|
761
|
+
(await page.evaluate(() => window.__hf?.duration ?? 0));
|
|
762
|
+
const total = Math.ceil(duration * fpsToNumber(session.options.fps));
|
|
763
|
+
return isStaticDedupFrameAnalysisSafe(total) ? total : 0;
|
|
764
|
+
}
|
|
676
765
|
/**
|
|
677
766
|
* The tail of drawElement init: self-verification ground truth (pre-injection),
|
|
678
767
|
* canvas injection, capture-mode flip, clip-boundary predictor, worker-encode.
|
|
@@ -712,9 +801,7 @@ async function finalizeDrawElementInit(session, page, logInitPhase, opts) {
|
|
|
712
801
|
// Worker-encode pipeline: macOS hardware GPU path only (syncToPaintEvent=true,
|
|
713
802
|
// beginFrameTimeTicks=0). Skip for BeginFrame (Linux/Docker) and transparent
|
|
714
803
|
// (PNG) output — those use the existing synchronous path unchanged.
|
|
715
|
-
const workerEncodeEnabled = (session
|
|
716
|
-
!transparent &&
|
|
717
|
-
session.beginFrameTimeTicks === 0;
|
|
804
|
+
const workerEncodeEnabled = drawElementWorkerEncodeEligible(session, transparent);
|
|
718
805
|
if (workerEncodeEnabled) {
|
|
719
806
|
await initDrawElementWorkerEncode(page);
|
|
720
807
|
session.workerEncodeEnabled = true;
|
|
@@ -730,13 +817,23 @@ export async function completeDeferredDrawElementInit(session) {
|
|
|
730
817
|
if (!session.deInitDeferred || !session.onBeforeCapture)
|
|
731
818
|
return;
|
|
732
819
|
const page = session.page;
|
|
733
|
-
const logInitPhase = (phase) => console.
|
|
820
|
+
const logInitPhase = (phase) => console.error(`[initSession:${session.captureMode}] ${phase} (deferred drawElement init)`);
|
|
734
821
|
await finalizeDrawElementInit(session, page, logInitPhase, {
|
|
735
|
-
transparent: session.options
|
|
822
|
+
transparent: capturesAlpha(session.options),
|
|
736
823
|
forceDE: process.env.HF_FORCE_DRAWELEMENT === "1",
|
|
737
824
|
});
|
|
738
825
|
session.deInitDeferred = false;
|
|
739
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Media elements are addressed by a document-unique render id, not by the per-file element id.
|
|
829
|
+
* Installed before any page script runs, on the render page and the self-check's truth page alike.
|
|
830
|
+
*/
|
|
831
|
+
async function installMediaPageHooks(page, options) {
|
|
832
|
+
await installMediaRenderIdBridge(page);
|
|
833
|
+
if (options.skipReadinessVideoIds?.length) {
|
|
834
|
+
await page.evaluateOnNewDocument(holdRenderFrameVideos, options.skipReadinessVideoIds, MEDIA_RENDER_ID_ATTR);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
740
837
|
// fallow-ignore-next-line unit-size
|
|
741
838
|
export async function createCaptureSession(serverUrl, outputDir, options, onBeforeCapture = null, config) {
|
|
742
839
|
if (!existsSync(outputDir))
|
|
@@ -755,7 +852,7 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
|
|
|
755
852
|
const isLinux = process.platform === "linux";
|
|
756
853
|
const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot;
|
|
757
854
|
const useDrawElement = config?.useDrawElement ?? false;
|
|
758
|
-
const drawElementTransparent = useDrawElement && options
|
|
855
|
+
const drawElementTransparent = useDrawElement && capturesAlpha(options);
|
|
759
856
|
// drawElement and page-side shader compositing are mutually incompatible
|
|
760
857
|
// capture strategies: drawElement reads the composition root's paint records
|
|
761
858
|
// directly and skips the prepare→micro-screenshot→resolve protocol (the
|
|
@@ -903,12 +1000,7 @@ async function constructCaptureSession(input) {
|
|
|
903
1000
|
w.__name = (fn, _name) => fn;
|
|
904
1001
|
}
|
|
905
1002
|
});
|
|
906
|
-
|
|
907
|
-
// per-file element id. Install the resolvers before any page script runs.
|
|
908
|
-
await installMediaRenderIdBridge(page);
|
|
909
|
-
if (options.skipReadinessVideoIds?.length) {
|
|
910
|
-
await page.evaluateOnNewDocument(holdRenderFrameVideos, options.skipReadinessVideoIds, MEDIA_RENDER_ID_ATTR);
|
|
911
|
-
}
|
|
1003
|
+
await installMediaPageHooks(page, options);
|
|
912
1004
|
// Fast capture: record accelerated canvases (webgl/webgl2/webgpu) and force
|
|
913
1005
|
// preserveDrawingBuffer before any page script can create a context — their
|
|
914
1006
|
// paint records freeze at the first frame, so captureDrawElementFrame
|
|
@@ -1693,6 +1785,16 @@ function recordConsoleScriptError(session, error) {
|
|
|
1693
1785
|
const message = new VfxFailureError(error).message;
|
|
1694
1786
|
recordCaptureWarnings(session, [{ code: "vfx_failure", message, details: { sources: [error] } }]);
|
|
1695
1787
|
}
|
|
1788
|
+
// Every worker page hits the same scene callback error; the render log names it once.
|
|
1789
|
+
function loggedEarlierInRender(session, text) {
|
|
1790
|
+
if (!text.startsWith(SCENE_CALLBACK_ERROR_PREFIX))
|
|
1791
|
+
return false;
|
|
1792
|
+
const logged = (session.subTimelineWaitMemo.loggedSceneCallbackErrors ??= []);
|
|
1793
|
+
if (logged.includes(text))
|
|
1794
|
+
return true;
|
|
1795
|
+
logged.push(text);
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1696
1798
|
function recordPageError(session, error) {
|
|
1697
1799
|
if (error && !session.pageErrors.includes(error))
|
|
1698
1800
|
session.pageErrors.push(error);
|
|
@@ -1747,14 +1849,15 @@ export function classifyPageError(details, projectScripts) {
|
|
|
1747
1849
|
export async function initializeSession(session) {
|
|
1748
1850
|
const { page, serverUrl } = session;
|
|
1749
1851
|
// Forward browser console to host. HyperFrames runtime logs get a dedicated
|
|
1750
|
-
// prefix so page-context observability is visible in producer
|
|
1852
|
+
// prefix so page-context observability is visible in producer stderr.
|
|
1751
1853
|
page.on("console", (msg) => {
|
|
1752
1854
|
const type = msg.type();
|
|
1753
1855
|
const text = msg.text();
|
|
1754
1856
|
const locationUrl = msg.location()?.url ?? "";
|
|
1755
1857
|
const diagnostic = formatConsoleDiagnostic(type, text, locationUrl);
|
|
1756
|
-
if (!diagnostic.suppressHostLog)
|
|
1757
|
-
console.
|
|
1858
|
+
if (!diagnostic.suppressHostLog && !loggedEarlierInRender(session, text)) {
|
|
1859
|
+
console.error(diagnostic.text);
|
|
1860
|
+
}
|
|
1758
1861
|
appendBrowserDiagnostic(session, diagnostic.text);
|
|
1759
1862
|
// A blocked script can never register its timeline (#3352); a thrown one may still, so it only counts on timeout.
|
|
1760
1863
|
const scriptFailure = classifyConsoleScriptFailure(type, text);
|
|
@@ -1818,7 +1921,7 @@ export async function initializeSession(session) {
|
|
|
1818
1921
|
const pageNavigationTimeout = session.config?.pageNavigationTimeout ?? DEFAULT_CONFIG.pageNavigationTimeout;
|
|
1819
1922
|
const initStart = Date.now();
|
|
1820
1923
|
const logInitPhase = (phase) => {
|
|
1821
|
-
console.
|
|
1924
|
+
console.error(`[initSession:${session.captureMode}] ${phase} (${Date.now() - initStart}ms)`);
|
|
1822
1925
|
};
|
|
1823
1926
|
const gotoEntryPage = async () => {
|
|
1824
1927
|
appendBrowserDiagnostic(session, formatNavigationStartDiagnostic({
|
|
@@ -2538,6 +2641,7 @@ export async function createStaticVerificationPage(session) {
|
|
|
2538
2641
|
w.__name = (fn, _name) => fn;
|
|
2539
2642
|
}
|
|
2540
2643
|
});
|
|
2644
|
+
await installMediaPageHooks(page, session.options);
|
|
2541
2645
|
if (session.options.variables && Object.keys(session.options.variables).length > 0) {
|
|
2542
2646
|
const variablesJson = JSON.stringify(session.options.variables);
|
|
2543
2647
|
await page.evaluateOnNewDocument((json) => {
|
|
@@ -2569,7 +2673,7 @@ export async function createStaticVerificationPage(session) {
|
|
|
2569
2673
|
page.evaluate(`document.fonts?.ready`),
|
|
2570
2674
|
waitForOptionalTailwindReady(page, pageReadyTimeout),
|
|
2571
2675
|
]);
|
|
2572
|
-
if (session.options
|
|
2676
|
+
if (capturesAlpha(session.options))
|
|
2573
2677
|
await initTransparentBackground(page);
|
|
2574
2678
|
return page;
|
|
2575
2679
|
}
|
|
@@ -3046,6 +3150,15 @@ async function redrawDrawElementFrame(session, frameIndex, time, seekOptions, he
|
|
|
3046
3150
|
await prepareFrameForCapture(session, frameIndex, time, seekOptions, heldVideoTime);
|
|
3047
3151
|
return captureDrawElementFrame(page, options.width, options.height, options.format ?? "jpeg", options.quality ?? 80, true);
|
|
3048
3152
|
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Fails the attempt on a frame in {@link CaptureSession.deRiskFrames}. Callers that hand those
|
|
3155
|
+
* frames to a screenshot session never reach it; any other caller restarts by screenshot.
|
|
3156
|
+
*/
|
|
3157
|
+
function assertNotRiskFrame(session, frameIndex) {
|
|
3158
|
+
if (session.deRiskFrames?.has(frameIndex)) {
|
|
3159
|
+
throw new DrawElementCaptureError(frameIndex, "frame needs screenshot capture, which this capture path cannot hand over");
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3049
3162
|
/** Seek to `time` and capture one surface with the session's capture mode. */
|
|
3050
3163
|
async function captureFrameSurface(session, frameIndex, time, seekOptions, heldVideoTime) {
|
|
3051
3164
|
const { page, options } = session;
|
|
@@ -3078,6 +3191,7 @@ async function captureFrameSurface(session, frameIndex, time, seekOptions, heldV
|
|
|
3078
3191
|
throw new DrawElementCaptureError(frameIndex, "boundary screenshot requested on an injected canvas page");
|
|
3079
3192
|
}
|
|
3080
3193
|
else if (session.captureMode === "drawelement") {
|
|
3194
|
+
assertNotRiskFrame(session, frameIndex);
|
|
3081
3195
|
// Advance compositor state via BeginFrame when available (Linux headless-shell);
|
|
3082
3196
|
// on macOS the compositor advances naturally without BeginFrame.
|
|
3083
3197
|
if (session.beginFrameTimeTicks > 0) {
|
|
@@ -3263,6 +3377,13 @@ async function captureAccumulatedFrame(session, frameIndex, absFrameIndex, plan)
|
|
|
3263
3377
|
...totals,
|
|
3264
3378
|
};
|
|
3265
3379
|
}
|
|
3380
|
+
function framesAreStatic(staticFrames, from, to) {
|
|
3381
|
+
for (let frame = from; frame <= to; frame++) {
|
|
3382
|
+
if (!staticFrames.has(frame))
|
|
3383
|
+
return false;
|
|
3384
|
+
}
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3266
3387
|
async function captureFrameCore(session, frameIndex, time) {
|
|
3267
3388
|
const { options } = session;
|
|
3268
3389
|
const startTime = Date.now();
|
|
@@ -3283,7 +3404,8 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
3283
3404
|
const absFrameIndex = Math.floor(time * fpsToNumber(options.fps) + 1e-9);
|
|
3284
3405
|
if (session.staticFrames?.has(absFrameIndex) &&
|
|
3285
3406
|
session.lastFrameBuffer &&
|
|
3286
|
-
session.lastFrameAbsoluteIndex
|
|
3407
|
+
session.lastFrameAbsoluteIndex !== undefined &&
|
|
3408
|
+
framesAreStatic(session.staticFrames, session.lastFrameAbsoluteIndex + 1, absFrameIndex) &&
|
|
3287
3409
|
(!session.motionBlur ||
|
|
3288
3410
|
motionBlurWindowIsStatic(session.motionBlur, absFrameIndex, session.staticFrames))) {
|
|
3289
3411
|
session.staticDedupCount = (session.staticDedupCount ?? 0) + 1;
|
|
@@ -3311,7 +3433,7 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
3311
3433
|
if (session.capturePerf.frames === 1) {
|
|
3312
3434
|
const blur = await describePageMotionBlur(session.page).catch(() => null);
|
|
3313
3435
|
if (blur)
|
|
3314
|
-
console.
|
|
3436
|
+
console.error(blur);
|
|
3315
3437
|
}
|
|
3316
3438
|
// Retain this freshly-captured buffer so the following static frames can reuse it.
|
|
3317
3439
|
if (session.staticFrames) {
|
|
@@ -3370,7 +3492,7 @@ export async function captureFrameToBuffer(session, frameIndex, time) {
|
|
|
3370
3492
|
// works. Counted here rather than in captureFrameCore's catch: the
|
|
3371
3493
|
// deadline rejects from outside it, so that catch never runs.
|
|
3372
3494
|
session.deFrameTimeouts = (session.deFrameTimeouts ?? 0) + 1;
|
|
3373
|
-
console.
|
|
3495
|
+
console.error(`[engine] fast capture frame ${frameIndex}: capture exceeded ` +
|
|
3374
3496
|
`${DE_FRAME_TIMEOUT_MS}ms; renderer stalled after drawElementImage ` +
|
|
3375
3497
|
`(PRINFRA-488). Failing the drawElement attempt so the whole render ` +
|
|
3376
3498
|
`retries via screenshot.`);
|
|
@@ -3403,6 +3525,7 @@ export async function captureFrameToBuffer(session, frameIndex, time) {
|
|
|
3403
3525
|
export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
3404
3526
|
const { page, options } = session;
|
|
3405
3527
|
const startTime = Date.now();
|
|
3528
|
+
assertNotRiskFrame(session, frameIndex);
|
|
3406
3529
|
// Task B: static-frame dedup (worker path). Reuse the prior frame's encode result
|
|
3407
3530
|
// and skip the seek + drawElement + encode entirely. Same predicate as the serial
|
|
3408
3531
|
// path; clip-cut frames are excluded from staticFrames so they always capture.
|
|
@@ -3412,13 +3535,7 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
3412
3535
|
// same pixels. Sequential capture reduces to has(i) (gap = {i}); the
|
|
3413
3536
|
// interleaved parallel stride makes the gap N frames wide.
|
|
3414
3537
|
const lastIdx = session.lastEncodeResultFrame ?? frameIndex - 1;
|
|
3415
|
-
|
|
3416
|
-
for (let j = lastIdx + 1; j <= frameIndex; j++) {
|
|
3417
|
-
if (!session.staticFrames.has(j)) {
|
|
3418
|
-
gapStatic = false;
|
|
3419
|
-
break;
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3538
|
+
const gapStatic = framesAreStatic(session.staticFrames, lastIdx + 1, frameIndex);
|
|
3422
3539
|
if (gapStatic) {
|
|
3423
3540
|
session.staticDedupCount = (session.staticDedupCount ?? 0) + 1;
|
|
3424
3541
|
session.capturePerf.frames += 1;
|
|
@@ -3527,6 +3644,8 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
|
|
|
3527
3644
|
throw new Error("[FrameCapture] Session not initialized");
|
|
3528
3645
|
}
|
|
3529
3646
|
assertVfxIntact(session);
|
|
3647
|
+
for (const frameIndex of frameIndices)
|
|
3648
|
+
assertNotRiskFrame(session, frameIndex);
|
|
3530
3649
|
const startTime = Date.now();
|
|
3531
3650
|
const fps = fpsToNumber(options.fps);
|
|
3532
3651
|
const quantized = times.map((t) => quantizeTimeToFrame(t, fps));
|
|
@@ -3572,7 +3691,7 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
|
|
|
3572
3691
|
throw new DrawElementCaptureError(frameIndices[failedAt] ?? failedAt, reason, error);
|
|
3573
3692
|
}
|
|
3574
3693
|
else {
|
|
3575
|
-
console.
|
|
3694
|
+
console.error(`[engine] fast capture: batch produce failed at frame ` +
|
|
3576
3695
|
`${frameIndices[failedAt] ?? "?"} (${error ?? "?"}); ` +
|
|
3577
3696
|
`re-capturing ${frameIndices.length - failedAt} frame(s) per-frame`);
|
|
3578
3697
|
for (let i = failedAt; i < frameIndices.length; i++) {
|
|
@@ -3651,7 +3770,7 @@ export async function closeCaptureSession(session) {
|
|
|
3651
3770
|
if (session.page && !session.pageReleased) {
|
|
3652
3771
|
const blur = await describePageMotionBlurAtEnd(session.page);
|
|
3653
3772
|
if (blur)
|
|
3654
|
-
console.
|
|
3773
|
+
console.error(blur);
|
|
3655
3774
|
}
|
|
3656
3775
|
// Realized static-dedup telemetry: how much the cache actually helped this
|
|
3657
3776
|
// render (vs the prediction logged at arm time). Both capture paths
|
|
@@ -3664,7 +3783,7 @@ export async function closeCaptureSession(session) {
|
|
|
3664
3783
|
const total = captured + reused;
|
|
3665
3784
|
const pct = total > 0 ? Math.round((reused / total) * 100) : 0;
|
|
3666
3785
|
const avgTotalMs = captured > 0 ? Math.round(session.capturePerf.totalMs / captured) : 0;
|
|
3667
|
-
console.
|
|
3786
|
+
console.error(`[static-dedup] reused ${reused}/${total} frame(s) (${pct}%), ` +
|
|
3668
3787
|
`est. ~${reused * avgTotalMs}ms saved (avg ${avgTotalMs}ms/frame)`);
|
|
3669
3788
|
session.staticDedupCount = 0;
|
|
3670
3789
|
}
|
|
@@ -3827,9 +3946,10 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
|
|
|
3827
3946
|
// would share the corruption, PSNR would pass on the damaged output.
|
|
3828
3947
|
// Seeking 0 → ascending reproduces the render's own seek order.
|
|
3829
3948
|
const fractions = computeDeVerifySampleFractions(k);
|
|
3830
|
-
//
|
|
3831
|
-
//
|
|
3832
|
-
const
|
|
3949
|
+
// The truth is read on a disposable page, so callbacks that run there cannot change the render's
|
|
3950
|
+
// own page. A video comp needs an injector of its own for it (the render's tracks its page).
|
|
3951
|
+
const truthInjector = session.onBeforeCapture?.fresh?.() ?? null;
|
|
3952
|
+
const disposable = session.onBeforeCapture && truthInjector === null
|
|
3833
3953
|
? null
|
|
3834
3954
|
: await createStaticVerificationPage(session).catch(() => {
|
|
3835
3955
|
logInitPhase("self-verify truth page unavailable; reading it on the render page");
|
|
@@ -3837,6 +3957,9 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
|
|
|
3837
3957
|
});
|
|
3838
3958
|
const isolated = disposable !== null;
|
|
3839
3959
|
const truthPage = disposable ?? page;
|
|
3960
|
+
const injectFrames = isolated
|
|
3961
|
+
? (truthInjector ?? session.onBeforeCapture)
|
|
3962
|
+
: session.onBeforeCapture;
|
|
3840
3963
|
const seekTo = (t) => seekDeVerifyTruth(truthPage, t, !isolated);
|
|
3841
3964
|
const frames = new Map();
|
|
3842
3965
|
try {
|
|
@@ -3848,9 +3971,10 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
|
|
|
3848
3971
|
// Nudge off clip-cut boundaries (±1-frame desync is legitimate there);
|
|
3849
3972
|
// if the nudge saturates on a boundary index, skip the sample entirely.
|
|
3850
3973
|
let guard = 0;
|
|
3851
|
-
|
|
3974
|
+
const skip = (i) => boundary.has(i) || session.deRiskFrames?.has(i) === true;
|
|
3975
|
+
while (skip(idx) && guard++ < 6)
|
|
3852
3976
|
idx = Math.min(totalFrames - 1, idx + 2);
|
|
3853
|
-
if (
|
|
3977
|
+
if (skip(idx))
|
|
3854
3978
|
continue;
|
|
3855
3979
|
if (frames.has(idx))
|
|
3856
3980
|
continue;
|
|
@@ -3859,8 +3983,7 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
|
|
|
3859
3983
|
// Video frame injection (same hook the real capture paths run) — without
|
|
3860
3984
|
// it, <video> elements screenshot black and every video comp would
|
|
3861
3985
|
// false-positive into the screenshot fallback.
|
|
3862
|
-
|
|
3863
|
-
await session.onBeforeCapture(truthPage, t);
|
|
3986
|
+
await injectFrames?.(truthPage, t);
|
|
3864
3987
|
// The first screenshot forces a frame, which runs rAF-driven callbacks (a count-up lands
|
|
3865
3988
|
// a tick after seek()); waiting on rAF via evaluate deadlocks, headless fires it only
|
|
3866
3989
|
// when a frame is produced.
|
|
@@ -3960,6 +4083,7 @@ export function getCapturePerfSummary(session) {
|
|
|
3960
4083
|
deVerifyArmed: session.deVerifyFrames?.size ?? 0,
|
|
3961
4084
|
deVerifyInitMs: session.deVerifyInitMs ?? 0,
|
|
3962
4085
|
deBoundaryFrames: session.clipBoundaryFrames?.size ?? 0,
|
|
4086
|
+
deRiskFrames: session.deRiskFrames?.size ?? 0,
|
|
3963
4087
|
deNcprFallbacks: ncprFallbacks,
|
|
3964
4088
|
deFrameTimeouts: session.deFrameTimeouts ?? 0,
|
|
3965
4089
|
};
|
|
@@ -358,7 +358,7 @@ async function captureFrameRange(session, task, captureOptions, signal, onFrameC
|
|
|
358
358
|
throw new Error("Parallel worker cancelled");
|
|
359
359
|
const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
|
|
360
360
|
if (dbg && i < task.startFrame + dbgWin) {
|
|
361
|
-
console.
|
|
361
|
+
console.error(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms produce ${i} start`);
|
|
362
362
|
}
|
|
363
363
|
const { encodeResult } = await runCaptureOperation("frame_capture", i, () => captureFrameToBufferPipelined(session, i - outputOffset, time));
|
|
364
364
|
// Marks the promise "handled" for Node's unhandled-rejection detector
|
|
@@ -368,19 +368,19 @@ async function captureFrameRange(session, task, captureOptions, signal, onFrameC
|
|
|
368
368
|
// and would otherwise surface as an unhandled rejection during teardown.
|
|
369
369
|
encodeResult.catch(() => { });
|
|
370
370
|
if (dbg && i < task.startFrame + dbgWin) {
|
|
371
|
-
console.
|
|
371
|
+
console.error(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms produce ${i} kicked`);
|
|
372
372
|
}
|
|
373
373
|
if (prev) {
|
|
374
374
|
if (dbg && prev.idx < task.startFrame + dbgWin) {
|
|
375
|
-
console.
|
|
375
|
+
console.error(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms drain ${prev.idx} await-encode`);
|
|
376
376
|
}
|
|
377
377
|
const buf = await awaitEncode(prev.idx, prev.encodeResult);
|
|
378
378
|
if (dbg && prev.idx < task.startFrame + dbgWin) {
|
|
379
|
-
console.
|
|
379
|
+
console.error(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms drain ${prev.idx} encoded ${buf.length}B`);
|
|
380
380
|
}
|
|
381
381
|
await onFrameBuffer(prev.idx, buf, session);
|
|
382
382
|
if (dbg && prev.idx < task.startFrame + dbgWin) {
|
|
383
|
-
console.
|
|
383
|
+
console.error(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms drain ${prev.idx} written`);
|
|
384
384
|
}
|
|
385
385
|
framesCaptured++;
|
|
386
386
|
if (onFrameCaptured)
|
|
@@ -453,7 +453,7 @@ export function selectVerifySampleIndicesForTask(sampleIndices, task) {
|
|
|
453
453
|
/** HF_DE_PAR_DEBUG=1 gated per-worker trace line (message built lazily). */
|
|
454
454
|
function logParDebug(message) {
|
|
455
455
|
if (process.env.HF_DE_PAR_DEBUG === "1")
|
|
456
|
-
console.
|
|
456
|
+
console.error(message());
|
|
457
457
|
}
|
|
458
458
|
/**
|
|
459
459
|
* Throw the verification error for a sample below the PSNR floor; log the
|
|
@@ -466,7 +466,7 @@ function assertDiskSampleAboveFloor(db, verifyMinDb, idx, workerId) {
|
|
|
466
466
|
throw new DrawElementVerificationError(`drawElement self-verify failed at frame ${idx} (disk path, worker ${workerId}): ` +
|
|
467
467
|
`${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot`, { kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb });
|
|
468
468
|
}
|
|
469
|
-
console.
|
|
469
|
+
console.error(`[Parallel] drawElement disk self-verify passed (worker ${workerId}, frame ${idx}, ` +
|
|
470
470
|
`${db === Infinity ? "inf" : db.toFixed(1)}dB)`);
|
|
471
471
|
}
|
|
472
472
|
/**
|
|
@@ -760,9 +760,11 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
|
|
|
760
760
|
};
|
|
761
761
|
const parallel = tasks.length > 1;
|
|
762
762
|
const deVerifySamples = resolveParallelDeVerifySamples(captureOptions.deVerifySamples, tasks.length);
|
|
763
|
-
const workerCaptureOptions =
|
|
764
|
-
|
|
765
|
-
|
|
763
|
+
const workerCaptureOptions = {
|
|
764
|
+
...captureOptions,
|
|
765
|
+
deVerifySamples,
|
|
766
|
+
deFrameFallback: false,
|
|
767
|
+
};
|
|
766
768
|
const peerController = new AbortController();
|
|
767
769
|
const workerSignal = signal
|
|
768
770
|
? AbortSignal.any([signal, peerController.signal])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which frames drawElement capture cannot reproduce, so those frames alone are captured by
|
|
3
|
+
* screenshot and the rest keep the fast path.
|
|
4
|
+
*/
|
|
5
|
+
import type { Page } from "puppeteer-core";
|
|
6
|
+
/**
|
|
7
|
+
* Above this share of risk frames the second browser and the switching cost outweigh the fast
|
|
8
|
+
* frames, so the whole render captures by screenshot as it always has.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_RISK_FRAME_FRACTION = 0.6;
|
|
11
|
+
/** Peak memory per-frame capture adds over a screenshot render at 1080p: 1002 to 2001 MB measured. */
|
|
12
|
+
export declare const RISK_RENDER_ADDED_BYTES = 1000000000;
|
|
13
|
+
/** Memory that must still be available once the screenshot browser is open. */
|
|
14
|
+
export declare const RISK_RENDER_HEADROOM_BYTES = 1500000000;
|
|
15
|
+
/** Risk ranges closer than this many frames are joined, so capture never alternates modes. */
|
|
16
|
+
export declare const RISK_RANGE_HYSTERESIS_FRAMES = 12;
|
|
17
|
+
/**
|
|
18
|
+
* Frames where a recorded effect element shows. Seeks the page forward, so use a throwaway one.
|
|
19
|
+
* Null once more than `maxFrames` frames qualify.
|
|
20
|
+
*/
|
|
21
|
+
export declare function scanCssEffectLiveFrames(page: Page, fps: number, totalFrames: number, maxFrames: number): Promise<Set<number> | null>;
|
|
22
|
+
/**
|
|
23
|
+
* The given frames with a one-frame margin, ranges closer than {@link RISK_RANGE_HYSTERESIS_FRAMES}
|
|
24
|
+
* joined. Null when they cover more than {@link MAX_RISK_FRAME_FRACTION} of the render.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planRiskFrames(frames: Iterable<number>, totalFrames: number): Set<number> | null;
|
|
27
|
+
/** `live` plus every frame from just before the first callback to the end of the render. */
|
|
28
|
+
export declare function withFramesAfterCallback(live: Set<number>, callbackFrom: number | null, fps: number, totalFrames: number): Set<number>;
|
|
29
|
+
/** Whether the render fits: what it adds, scaled by output pixels, leaves the headroom. */
|
|
30
|
+
export declare function riskSessionFits(availableMb: number, outputPixels: number): boolean;
|