@hyperframes/engine 0.8.15 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +11 -9
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/chunkEncoder.d.ts +7 -1
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +12 -8
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/drawElementService.d.ts +25 -0
- package/dist/services/drawElementService.d.ts.map +1 -1
- package/dist/services/drawElementService.js +49 -6
- package/dist/services/drawElementService.js.map +1 -1
- package/dist/services/extractedFrameIndex.d.ts +8 -0
- package/dist/services/extractedFrameIndex.d.ts.map +1 -0
- package/dist/services/extractedFrameIndex.js +43 -0
- package/dist/services/extractedFrameIndex.js.map +1 -0
- package/dist/services/extractionCache.d.ts +6 -1
- package/dist/services/extractionCache.d.ts.map +1 -1
- package/dist/services/extractionCache.js +26 -10
- package/dist/services/extractionCache.js.map +1 -1
- package/dist/services/frameCapture.d.ts +18 -6
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +184 -33
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/referenceResolver.d.ts +1 -2
- package/dist/services/referenceResolver.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts +12 -0
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +25 -20
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/utils/ffprobe.d.ts +5 -0
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +9 -0
- package/dist/utils/ffprobe.js.map +1 -1
- package/package.json +3 -3
|
@@ -13,7 +13,7 @@ import { quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, } from "
|
|
|
13
13
|
// ── Extracted modules ───────────────────────────────────────────────────────
|
|
14
14
|
import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, resolveHeadlessShellPath, } from "./browserManager.js";
|
|
15
15
|
import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
|
|
16
|
-
import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, } from "./drawElementService.js";
|
|
16
|
+
import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, DE_CANVAS_NOT_INITIALIZED_CODE, } from "./drawElementService.js";
|
|
17
17
|
import { initThreeDProjection, detectCssEffectRisk } from "./threeDProjection.js";
|
|
18
18
|
import { isPsnrFilterAvailable } from "../utils/psnrFilterAvailability.js";
|
|
19
19
|
import { DEFAULT_CONFIG, applyConcreteGpuScreenshotClamp } from "../config.js";
|
|
@@ -1330,13 +1330,16 @@ function recordSubTimelineWarning(session, timeoutMs) {
|
|
|
1330
1330
|
if (session.subTimelineWaitOutcome === "ready" || !session.subTimelineWaitOutcome)
|
|
1331
1331
|
return;
|
|
1332
1332
|
const scriptFailure = session.subTimelineWaitOutcome === "script_failure";
|
|
1333
|
+
const hasRuntimeErrors = session.scriptLoadFailures.some((f) => f.startsWith("runtime-error:"));
|
|
1333
1334
|
recordCaptureWarnings(session, [
|
|
1334
1335
|
{
|
|
1335
1336
|
code: scriptFailure ? "sub_timeline_script_failure" : "sub_timeline_readiness_timeout",
|
|
1336
1337
|
message: scriptFailure
|
|
1337
|
-
?
|
|
1338
|
+
? hasRuntimeErrors
|
|
1339
|
+
? `A sub-composition script threw during execution — timeline registration never arrived (${session.scriptLoadFailures.join(", ")})`
|
|
1340
|
+
: `A sub-composition timeline script failed to load (${session.scriptLoadFailures.join(", ")})`
|
|
1338
1341
|
: `Sub-composition timelines did not become ready within ${timeoutMs}ms`,
|
|
1339
|
-
details: { timeoutMs },
|
|
1342
|
+
details: { timeoutMs, sources: [...session.scriptLoadFailures] },
|
|
1340
1343
|
},
|
|
1341
1344
|
]);
|
|
1342
1345
|
}
|
|
@@ -1478,6 +1481,15 @@ export async function initializeSession(session) {
|
|
|
1478
1481
|
if (!diagnostic.suppressHostLog)
|
|
1479
1482
|
console.log(diagnostic.text);
|
|
1480
1483
|
appendBrowserDiagnostic(session, diagnostic.text);
|
|
1484
|
+
// Composition script runtime errors mean the GSAP timeline registration
|
|
1485
|
+
// can never arrive — same fail-fast treatment as script load failures.
|
|
1486
|
+
// Without this, pollSubCompositionTimelines burns the full timeout and
|
|
1487
|
+
// the render silently succeeds with a degenerate 2-frame output (#3352).
|
|
1488
|
+
if (type === "error" && text.startsWith("[HyperFrames] composition script error:")) {
|
|
1489
|
+
const detail = text.slice("[HyperFrames] composition script error:".length).trim();
|
|
1490
|
+
const compId = detail.split(" ")[0] || "unknown";
|
|
1491
|
+
recordScriptLoadFailure(session, `runtime-error:${compId}`);
|
|
1492
|
+
}
|
|
1481
1493
|
});
|
|
1482
1494
|
page.on("pageerror", (err) => {
|
|
1483
1495
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2475,10 +2487,49 @@ async function computeTimelineAtRiskFrames(page, fps) {
|
|
|
2475
2487
|
* thrown when a subtree element has no paint record for the current frame (display
|
|
2476
2488
|
* toggled / detached / freshly-shown at a clip-cut boundary). Per-frame, not
|
|
2477
2489
|
* whole-comp — callers fall back to screenshot for the single frame.
|
|
2490
|
+
*
|
|
2491
|
+
* This is a NATIVE Chrome DOMException (`drawElementImage`'s own error), so we
|
|
2492
|
+
* can't bake a discriminant into it the way we can for our own thrown errors
|
|
2493
|
+
* (see {@link isCanvasNotInitializedError}) — Puppeteer's `page.evaluate`
|
|
2494
|
+
* error reconstruction also doesn't preserve a usable `.name` for it (comes
|
|
2495
|
+
* back generic). Match on the FULL native phrase ("...for element"), not just
|
|
2496
|
+
* the generic "No cached paint record" prefix, to cut the odds of an
|
|
2497
|
+
* unrelated message coincidentally matching (review: substring-match footgun).
|
|
2478
2498
|
*/
|
|
2479
2499
|
function isNoCachedPaintRecordError(err) {
|
|
2480
2500
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2481
|
-
return msg.includes("No cached paint record");
|
|
2501
|
+
return msg.includes("No cached paint record for element");
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* True for the drawElement "capture canvas isn't set up yet" error — thrown
|
|
2505
|
+
* (or, on the batch path, returned as a string) by drawElementService when
|
|
2506
|
+
* the injected capture canvas (`#__hf_de_canvas`) isn't set up yet (observed
|
|
2507
|
+
* at frame 0 on some macOS/Chrome combinations, see #3423). Recoverable:
|
|
2508
|
+
* the composition root IS present, so `pageScreenshotCapture` captures valid
|
|
2509
|
+
* content.
|
|
2510
|
+
*
|
|
2511
|
+
* This is distinct from the composition-root-missing case
|
|
2512
|
+
* (`HF_DE_COMPOSITION_ROOT_MISSING`), which is NOT recoverable — the page
|
|
2513
|
+
* has no composition content to screenshot, so falling back would capture
|
|
2514
|
+
* blank or navigated-away content.
|
|
2515
|
+
*
|
|
2516
|
+
* Matches the {@link DE_CANVAS_NOT_INITIALIZED_CODE} discriminant baked into
|
|
2517
|
+
* the message (not free-text), so it survives `produceDrawElementFrameBatch`'s
|
|
2518
|
+
* "batch produce failed at frame N: <code>: ..." wrapping.
|
|
2519
|
+
*/
|
|
2520
|
+
function isCanvasNotInitializedError(err) {
|
|
2521
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2522
|
+
return msg.includes(DE_CANVAS_NOT_INITIALIZED_CODE);
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Single gate for drawElement failures the fast-capture pipeline knows how to
|
|
2526
|
+
* recover from by falling back to screenshot capture instead of aborting the
|
|
2527
|
+
* render. Both {@link captureFrameCore} and {@link captureFrameToBufferPipelined}
|
|
2528
|
+
* consult this so a newly-recognized recoverable error only needs to be taught
|
|
2529
|
+
* here once.
|
|
2530
|
+
*/
|
|
2531
|
+
function isRecoverableDrawElementError(err) {
|
|
2532
|
+
return isNoCachedPaintRecordError(err) || isCanvasNotInitializedError(err);
|
|
2482
2533
|
}
|
|
2483
2534
|
async function captureFrameCore(session, frameIndex, time) {
|
|
2484
2535
|
const { page, options } = session;
|
|
@@ -2532,7 +2583,7 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2532
2583
|
// stale), so the "fallback" REPLACES good frames with damaged ones (validated:
|
|
2533
2584
|
// 35e8fa9f 462→0 damaged frames, 4001da8e 11→0, when this is off). The two real
|
|
2534
2585
|
// boundary failure modes are now caught reactively below — the throw case by
|
|
2535
|
-
//
|
|
2586
|
+
// isRecoverableDrawElementError, the silent-solid-black case by the small-frame
|
|
2536
2587
|
// blank-guard (a solid frame is a tiny JPEG) — without touching frames drawElement
|
|
2537
2588
|
// handles. Force the old behavior with HF_FAST_CAPTURE_BOUNDARY_SS=true. The worker
|
|
2538
2589
|
// path keeps proactive boundary-SS (it has no blank-guard); see
|
|
@@ -2586,12 +2637,17 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2586
2637
|
catch (err) {
|
|
2587
2638
|
// drawElementImage throws `InvalidStateError: No cached paint record for
|
|
2588
2639
|
// element` when an element in the subtree has no paint record this frame
|
|
2589
|
-
// (display toggled / detached / freshly-shown at a clip-cut boundary)
|
|
2590
|
-
//
|
|
2591
|
-
//
|
|
2592
|
-
|
|
2640
|
+
// (display toggled / detached / freshly-shown at a clip-cut boundary), and
|
|
2641
|
+
// `canvas not initialized` when the injected capture canvas isn't set up yet
|
|
2642
|
+
// (observed at frame 0 on some macOS/Chrome combinations, see #3423). Both
|
|
2643
|
+
// are per-frame conditions, not whole-comp ones — fall back to screenshot for
|
|
2644
|
+
// THIS frame instead of aborting the render. See fast-capture-limitations.md.
|
|
2645
|
+
if (isRecoverableDrawElementError(err)) {
|
|
2593
2646
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2594
|
-
|
|
2647
|
+
const reason = isCanvasNotInitializedError(err)
|
|
2648
|
+
? "drawElement canvas not initialized"
|
|
2649
|
+
: "No cached paint record";
|
|
2650
|
+
console.log(`[engine] fast capture: frame ${frameIndex} — ${reason}; ` +
|
|
2595
2651
|
`screenshot fallback for this frame (see fast-capture-limitations.md)`);
|
|
2596
2652
|
screenshotBuffer = await pageScreenshotCapture(page, options);
|
|
2597
2653
|
}
|
|
@@ -2749,13 +2805,17 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
2749
2805
|
return { encodeResult, captureTimeMs };
|
|
2750
2806
|
}
|
|
2751
2807
|
catch (captureError) {
|
|
2752
|
-
// Per-frame `No cached paint record
|
|
2753
|
-
// instead of aborting the render (clip-cut
|
|
2754
|
-
//
|
|
2755
|
-
//
|
|
2756
|
-
|
|
2808
|
+
// Per-frame `No cached paint record` or `canvas not initialized` (#3423): fall
|
|
2809
|
+
// back to screenshot for THIS frame instead of aborting the render (clip-cut
|
|
2810
|
+
// boundary / freshly-shown element / capture canvas not yet set up). The worker
|
|
2811
|
+
// isn't involved for this frame; return a resolved encodeResult so the pipeline
|
|
2812
|
+
// loop writes it like any other. See fast-capture-limitations.md.
|
|
2813
|
+
if (isRecoverableDrawElementError(captureError)) {
|
|
2757
2814
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2758
|
-
|
|
2815
|
+
const reason = isCanvasNotInitializedError(captureError)
|
|
2816
|
+
? "drawElement canvas not initialized"
|
|
2817
|
+
: "No cached paint record";
|
|
2818
|
+
console.log(`[engine] fast capture: frame ${frameIndex} — ${reason}; ` +
|
|
2759
2819
|
`screenshot fallback for this frame (see fast-capture-limitations.md)`);
|
|
2760
2820
|
const buffer = await pageScreenshotCapture(page, options);
|
|
2761
2821
|
return { encodeResult: Promise.resolve(buffer), captureTimeMs: Date.now() - startTime };
|
|
@@ -2776,8 +2836,9 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
2776
2836
|
* drain time:
|
|
2777
2837
|
* - the static-dedup fast path returns session.lastEncodeResult, which by
|
|
2778
2838
|
* drain time can hold a frame several indices AHEAD of the suspect frame;
|
|
2779
|
-
* - the per-frame "No cached paint
|
|
2780
|
-
*
|
|
2839
|
+
* - the per-frame recoverable-error screenshot fallback ("No cached paint
|
|
2840
|
+
* record" or "canvas not initialized") captures the viewport — which may
|
|
2841
|
+
* hold the LAST drawn drawElement frame, not this one.
|
|
2781
2842
|
* Any failure here throws; the caller treats that as verification failure and
|
|
2782
2843
|
* falls back the whole render (correct, never wrong-frame).
|
|
2783
2844
|
*/
|
|
@@ -2794,10 +2855,21 @@ export async function recaptureDrawElementFrameForVerify(session, frameIndex, ti
|
|
|
2794
2855
|
* P6 prototype (HF_DE_BATCH): capture N consecutive frames in one CDP
|
|
2795
2856
|
* round-trip via {@link produceDrawElementFrameBatch}. The caller pre-plans the
|
|
2796
2857
|
* batch (consecutive frame indices, none static-dedup'd, none opt-in
|
|
2797
|
-
* boundary-screenshot). On a mid-batch in-page failure the remaining frames
|
|
2798
|
-
*
|
|
2799
|
-
* per-frame
|
|
2800
|
-
*
|
|
2858
|
+
* boundary-screenshot). On a mid-batch in-page failure the remaining frames'
|
|
2859
|
+
* handling depends on whether the failure is one of the recoverable
|
|
2860
|
+
* per-frame drawElement conditions (canvas-not-initialized / no-cached-paint-
|
|
2861
|
+
* record, #3423):
|
|
2862
|
+
* - Recoverable: capture the remaining frames directly via screenshot,
|
|
2863
|
+
* same as the per-frame paths' own fallback (avoids re-attempting a
|
|
2864
|
+
* drawElement produce that the batch call just told us will fail again —
|
|
2865
|
+
* review finding: audit this path explicitly rather than relying on the
|
|
2866
|
+
* incidental retry-then-catch behavior below).
|
|
2867
|
+
* - Anything else (unrecognized error): fall through to
|
|
2868
|
+
* {@link captureFrameToBufferPipelined}, which re-attempts drawElement (so
|
|
2869
|
+
* a genuinely transient, non-drawElement-specific failure still gets a
|
|
2870
|
+
* second chance) and owns the same recoverable-error/fatal-error split for
|
|
2871
|
+
* whatever it encounters — so failure behavior for a truly fatal error is
|
|
2872
|
+
* identical to the unbatched path, just discovered at batch granularity.
|
|
2801
2873
|
*/
|
|
2802
2874
|
export async function captureFramesBatchPipelined(session, frameIndices, times) {
|
|
2803
2875
|
const { page, options } = session;
|
|
@@ -2829,16 +2901,57 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
|
|
|
2829
2901
|
results.push({ frameIndex, encodeResult });
|
|
2830
2902
|
}
|
|
2831
2903
|
if (failedAt !== null) {
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2904
|
+
// `error` is a plain string here (produceDrawElementFrameBatch returns it
|
|
2905
|
+
// out of an in-page evaluate rather than throwing an Error instance) —
|
|
2906
|
+
// isRecoverableDrawElementError accepts `unknown` and stringifies non-Error
|
|
2907
|
+
// input, so passing the string straight through classifies it correctly,
|
|
2908
|
+
// including through produceDrawElementFrameBatch's own error text (which
|
|
2909
|
+
// embeds the same DE_CANVAS_NOT_INITIALIZED_CODE / native paint-record
|
|
2910
|
+
// phrase the per-frame paths match on).
|
|
2911
|
+
if (isRecoverableDrawElementError(error)) {
|
|
2912
|
+
const reason = isCanvasNotInitializedError(error)
|
|
2913
|
+
? "drawElement canvas not initialized"
|
|
2914
|
+
: "No cached paint record";
|
|
2915
|
+
console.log(`[engine] fast capture: batch produce failed at frame ` +
|
|
2916
|
+
`${frameIndices[failedAt] ?? "?"} (${reason}); ` +
|
|
2917
|
+
`screenshot fallback for ${frameIndices.length - failedAt} frame(s) ` +
|
|
2918
|
+
`(see fast-capture-limitations.md)`);
|
|
2919
|
+
for (let i = failedAt; i < frameIndices.length; i++) {
|
|
2920
|
+
const frameIndex = frameIndices[i];
|
|
2921
|
+
const time = times[i];
|
|
2922
|
+
if (frameIndex === undefined || time === undefined)
|
|
2923
|
+
break;
|
|
2924
|
+
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2925
|
+
// Each remaining frame still needs its own seek/prepare — the batch
|
|
2926
|
+
// produce call left the page composited for whichever frame it last
|
|
2927
|
+
// attempted, not this one. Without this, every fallback screenshot in
|
|
2928
|
+
// the loop captures the SAME (stale) frame instead of advancing.
|
|
2929
|
+
// Deliberately reuse prepareFrameForCapture rather than routing
|
|
2930
|
+
// through captureFrameToBufferPipelined here, since that would
|
|
2931
|
+
// re-attempt produceDrawElementFrame — which the batch call already
|
|
2932
|
+
// told us will fail again for these frames (see function doc above).
|
|
2933
|
+
await prepareFrameForCapture(session, frameIndex, time);
|
|
2934
|
+
const buffer = await pageScreenshotCapture(page, options);
|
|
2935
|
+
const encodeResult = Promise.resolve(buffer);
|
|
2936
|
+
if (session.staticFrames) {
|
|
2937
|
+
session.lastEncodeResult = encodeResult;
|
|
2938
|
+
session.lastEncodeResultFrame = frameIndex;
|
|
2939
|
+
}
|
|
2940
|
+
results.push({ frameIndex, encodeResult });
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
console.log(`[engine] fast capture: batch produce failed at frame ` +
|
|
2945
|
+
`${frameIndices[failedAt] ?? "?"} (${error ?? "?"}); ` +
|
|
2946
|
+
`re-capturing ${frameIndices.length - failedAt} frame(s) per-frame`);
|
|
2947
|
+
for (let i = failedAt; i < frameIndices.length; i++) {
|
|
2948
|
+
const frameIndex = frameIndices[i];
|
|
2949
|
+
const time = times[i];
|
|
2950
|
+
if (frameIndex === undefined || time === undefined)
|
|
2951
|
+
break;
|
|
2952
|
+
const { encodeResult } = await captureFrameToBufferPipelined(session, frameIndex, time);
|
|
2953
|
+
results.push({ frameIndex, encodeResult });
|
|
2954
|
+
}
|
|
2842
2955
|
}
|
|
2843
2956
|
}
|
|
2844
2957
|
// Task B: retain the last encode result so a following static frame can reuse it.
|
|
@@ -3137,8 +3250,46 @@ export function percentileOf(samples, p) {
|
|
|
3137
3250
|
const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(p * sorted.length)));
|
|
3138
3251
|
return Math.round(sorted[idx] ?? 0);
|
|
3139
3252
|
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Fraction of captured frames above which a fast-capture render is treated as
|
|
3255
|
+
* "drawElement effectively didn't engage" rather than "recovered a handful of
|
|
3256
|
+
* edge-case frames" (see the cross-PR-seam warning in
|
|
3257
|
+
* {@link getCapturePerfSummary}). Not currently a hard gate — see that
|
|
3258
|
+
* function's comment for why — just the threshold for the loud diagnostic.
|
|
3259
|
+
*/
|
|
3260
|
+
const DE_FALLBACK_RATIO_WARN_THRESHOLD = 0.5;
|
|
3140
3261
|
export function getCapturePerfSummary(session) {
|
|
3141
3262
|
const frames = Math.max(1, session.capturePerf.frames);
|
|
3263
|
+
const ncprFallbacks = session.deNcprFallbacks ?? 0;
|
|
3264
|
+
// Cross-PR seam (#3423 per-frame screenshot fallback vs #3429 artifact
|
|
3265
|
+
// validation): #3429's artifact validation only checks that the render
|
|
3266
|
+
// produced the right frame COUNT and duration — it has no visibility into
|
|
3267
|
+
// HOW each frame was captured. If a composition is so incompatible with
|
|
3268
|
+
// drawElement that most/all frames take the per-frame screenshot fallback
|
|
3269
|
+
// added here, the render still reports "complete" with a correct frame
|
|
3270
|
+
// count, even though drawElement effectively never engaged for it. That's
|
|
3271
|
+
// not itself a correctness bug — screenshot capture is the platform's
|
|
3272
|
+
// normal, well-tested baseline, so the SHIPPED PIXELS are fine — but a
|
|
3273
|
+
// near-100% fallback ratio is a strong signal that fast-capture silently
|
|
3274
|
+
// failed to engage for the whole render (e.g. a persistent canvas-injection
|
|
3275
|
+
// problem) rather than recovering a handful of expected edge-case frames,
|
|
3276
|
+
// and today nothing surfaces that distinction to telemetry or to a human.
|
|
3277
|
+
//
|
|
3278
|
+
// Deliberately NOT a circuit breaker: aborting/failing the render here
|
|
3279
|
+
// would make a render that reliably succeeds via the well-tested screenshot
|
|
3280
|
+
// path fail instead, which is a worse outcome than a slow-but-correct
|
|
3281
|
+
// render. Whether artifact validation (or this session) should eventually
|
|
3282
|
+
// gate on the ratio — and where that decision belongs — is tracked as an
|
|
3283
|
+
// explicit follow-up: https://github.com/heygen-com/hyperframes/issues/3482
|
|
3284
|
+
// ("Fast-capture: fallback-ratio guard for #3423 x #3429 seam"), rather
|
|
3285
|
+
// than decided unilaterally in this review-response commit.
|
|
3286
|
+
if (frames > 0 && ncprFallbacks / frames > DE_FALLBACK_RATIO_WARN_THRESHOLD) {
|
|
3287
|
+
const pct = Math.round((ncprFallbacks / frames) * 100);
|
|
3288
|
+
console.warn(`[engine] fast capture: ${ncprFallbacks}/${frames} frame(s) (${pct}%) fell back to ` +
|
|
3289
|
+
`screenshot capture (canvas-not-initialized / no-cached-paint-record) — ` +
|
|
3290
|
+
`drawElement likely failed to engage for this render rather than recovering a few ` +
|
|
3291
|
+
`edge-case frames; see fast-capture-limitations.md.`);
|
|
3292
|
+
}
|
|
3142
3293
|
return {
|
|
3143
3294
|
frames: session.capturePerf.frames,
|
|
3144
3295
|
avgTotalMs: Math.round(session.capturePerf.totalMs / frames),
|
|
@@ -3177,7 +3328,7 @@ export function getCapturePerfSummary(session) {
|
|
|
3177
3328
|
deVerifyArmed: session.deVerifyFrames?.size ?? 0,
|
|
3178
3329
|
deVerifyInitMs: session.deVerifyInitMs ?? 0,
|
|
3179
3330
|
deBoundaryFrames: session.clipBoundaryFrames?.size ?? 0,
|
|
3180
|
-
deNcprFallbacks:
|
|
3331
|
+
deNcprFallbacks: ncprFallbacks,
|
|
3181
3332
|
};
|
|
3182
3333
|
}
|
|
3183
3334
|
//# sourceMappingURL=frameCapture.js.map
|