@hyperframes/engine 0.8.15 → 0.8.16
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 +170 -31
- 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/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";
|
|
@@ -2475,10 +2475,49 @@ async function computeTimelineAtRiskFrames(page, fps) {
|
|
|
2475
2475
|
* thrown when a subtree element has no paint record for the current frame (display
|
|
2476
2476
|
* toggled / detached / freshly-shown at a clip-cut boundary). Per-frame, not
|
|
2477
2477
|
* whole-comp — callers fall back to screenshot for the single frame.
|
|
2478
|
+
*
|
|
2479
|
+
* This is a NATIVE Chrome DOMException (`drawElementImage`'s own error), so we
|
|
2480
|
+
* can't bake a discriminant into it the way we can for our own thrown errors
|
|
2481
|
+
* (see {@link isCanvasNotInitializedError}) — Puppeteer's `page.evaluate`
|
|
2482
|
+
* error reconstruction also doesn't preserve a usable `.name` for it (comes
|
|
2483
|
+
* back generic). Match on the FULL native phrase ("...for element"), not just
|
|
2484
|
+
* the generic "No cached paint record" prefix, to cut the odds of an
|
|
2485
|
+
* unrelated message coincidentally matching (review: substring-match footgun).
|
|
2478
2486
|
*/
|
|
2479
2487
|
function isNoCachedPaintRecordError(err) {
|
|
2480
2488
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2481
|
-
return msg.includes("No cached paint record");
|
|
2489
|
+
return msg.includes("No cached paint record for element");
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* True for the drawElement "capture canvas isn't set up yet" error — thrown
|
|
2493
|
+
* (or, on the batch path, returned as a string) by drawElementService when
|
|
2494
|
+
* the injected capture canvas (`#__hf_de_canvas`) isn't set up yet (observed
|
|
2495
|
+
* at frame 0 on some macOS/Chrome combinations, see #3423). Recoverable:
|
|
2496
|
+
* the composition root IS present, so `pageScreenshotCapture` captures valid
|
|
2497
|
+
* content.
|
|
2498
|
+
*
|
|
2499
|
+
* This is distinct from the composition-root-missing case
|
|
2500
|
+
* (`HF_DE_COMPOSITION_ROOT_MISSING`), which is NOT recoverable — the page
|
|
2501
|
+
* has no composition content to screenshot, so falling back would capture
|
|
2502
|
+
* blank or navigated-away content.
|
|
2503
|
+
*
|
|
2504
|
+
* Matches the {@link DE_CANVAS_NOT_INITIALIZED_CODE} discriminant baked into
|
|
2505
|
+
* the message (not free-text), so it survives `produceDrawElementFrameBatch`'s
|
|
2506
|
+
* "batch produce failed at frame N: <code>: ..." wrapping.
|
|
2507
|
+
*/
|
|
2508
|
+
function isCanvasNotInitializedError(err) {
|
|
2509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2510
|
+
return msg.includes(DE_CANVAS_NOT_INITIALIZED_CODE);
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Single gate for drawElement failures the fast-capture pipeline knows how to
|
|
2514
|
+
* recover from by falling back to screenshot capture instead of aborting the
|
|
2515
|
+
* render. Both {@link captureFrameCore} and {@link captureFrameToBufferPipelined}
|
|
2516
|
+
* consult this so a newly-recognized recoverable error only needs to be taught
|
|
2517
|
+
* here once.
|
|
2518
|
+
*/
|
|
2519
|
+
function isRecoverableDrawElementError(err) {
|
|
2520
|
+
return isNoCachedPaintRecordError(err) || isCanvasNotInitializedError(err);
|
|
2482
2521
|
}
|
|
2483
2522
|
async function captureFrameCore(session, frameIndex, time) {
|
|
2484
2523
|
const { page, options } = session;
|
|
@@ -2532,7 +2571,7 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2532
2571
|
// stale), so the "fallback" REPLACES good frames with damaged ones (validated:
|
|
2533
2572
|
// 35e8fa9f 462→0 damaged frames, 4001da8e 11→0, when this is off). The two real
|
|
2534
2573
|
// boundary failure modes are now caught reactively below — the throw case by
|
|
2535
|
-
//
|
|
2574
|
+
// isRecoverableDrawElementError, the silent-solid-black case by the small-frame
|
|
2536
2575
|
// blank-guard (a solid frame is a tiny JPEG) — without touching frames drawElement
|
|
2537
2576
|
// handles. Force the old behavior with HF_FAST_CAPTURE_BOUNDARY_SS=true. The worker
|
|
2538
2577
|
// path keeps proactive boundary-SS (it has no blank-guard); see
|
|
@@ -2586,12 +2625,17 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2586
2625
|
catch (err) {
|
|
2587
2626
|
// drawElementImage throws `InvalidStateError: No cached paint record for
|
|
2588
2627
|
// 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
|
-
|
|
2628
|
+
// (display toggled / detached / freshly-shown at a clip-cut boundary), and
|
|
2629
|
+
// `canvas not initialized` when the injected capture canvas isn't set up yet
|
|
2630
|
+
// (observed at frame 0 on some macOS/Chrome combinations, see #3423). Both
|
|
2631
|
+
// are per-frame conditions, not whole-comp ones — fall back to screenshot for
|
|
2632
|
+
// THIS frame instead of aborting the render. See fast-capture-limitations.md.
|
|
2633
|
+
if (isRecoverableDrawElementError(err)) {
|
|
2593
2634
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2594
|
-
|
|
2635
|
+
const reason = isCanvasNotInitializedError(err)
|
|
2636
|
+
? "drawElement canvas not initialized"
|
|
2637
|
+
: "No cached paint record";
|
|
2638
|
+
console.log(`[engine] fast capture: frame ${frameIndex} — ${reason}; ` +
|
|
2595
2639
|
`screenshot fallback for this frame (see fast-capture-limitations.md)`);
|
|
2596
2640
|
screenshotBuffer = await pageScreenshotCapture(page, options);
|
|
2597
2641
|
}
|
|
@@ -2749,13 +2793,17 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
2749
2793
|
return { encodeResult, captureTimeMs };
|
|
2750
2794
|
}
|
|
2751
2795
|
catch (captureError) {
|
|
2752
|
-
// Per-frame `No cached paint record
|
|
2753
|
-
// instead of aborting the render (clip-cut
|
|
2754
|
-
//
|
|
2755
|
-
//
|
|
2756
|
-
|
|
2796
|
+
// Per-frame `No cached paint record` or `canvas not initialized` (#3423): fall
|
|
2797
|
+
// back to screenshot for THIS frame instead of aborting the render (clip-cut
|
|
2798
|
+
// boundary / freshly-shown element / capture canvas not yet set up). The worker
|
|
2799
|
+
// isn't involved for this frame; return a resolved encodeResult so the pipeline
|
|
2800
|
+
// loop writes it like any other. See fast-capture-limitations.md.
|
|
2801
|
+
if (isRecoverableDrawElementError(captureError)) {
|
|
2757
2802
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2758
|
-
|
|
2803
|
+
const reason = isCanvasNotInitializedError(captureError)
|
|
2804
|
+
? "drawElement canvas not initialized"
|
|
2805
|
+
: "No cached paint record";
|
|
2806
|
+
console.log(`[engine] fast capture: frame ${frameIndex} — ${reason}; ` +
|
|
2759
2807
|
`screenshot fallback for this frame (see fast-capture-limitations.md)`);
|
|
2760
2808
|
const buffer = await pageScreenshotCapture(page, options);
|
|
2761
2809
|
return { encodeResult: Promise.resolve(buffer), captureTimeMs: Date.now() - startTime };
|
|
@@ -2776,8 +2824,9 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
2776
2824
|
* drain time:
|
|
2777
2825
|
* - the static-dedup fast path returns session.lastEncodeResult, which by
|
|
2778
2826
|
* drain time can hold a frame several indices AHEAD of the suspect frame;
|
|
2779
|
-
* - the per-frame "No cached paint
|
|
2780
|
-
*
|
|
2827
|
+
* - the per-frame recoverable-error screenshot fallback ("No cached paint
|
|
2828
|
+
* record" or "canvas not initialized") captures the viewport — which may
|
|
2829
|
+
* hold the LAST drawn drawElement frame, not this one.
|
|
2781
2830
|
* Any failure here throws; the caller treats that as verification failure and
|
|
2782
2831
|
* falls back the whole render (correct, never wrong-frame).
|
|
2783
2832
|
*/
|
|
@@ -2794,10 +2843,21 @@ export async function recaptureDrawElementFrameForVerify(session, frameIndex, ti
|
|
|
2794
2843
|
* P6 prototype (HF_DE_BATCH): capture N consecutive frames in one CDP
|
|
2795
2844
|
* round-trip via {@link produceDrawElementFrameBatch}. The caller pre-plans the
|
|
2796
2845
|
* 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
|
-
*
|
|
2846
|
+
* boundary-screenshot). On a mid-batch in-page failure the remaining frames'
|
|
2847
|
+
* handling depends on whether the failure is one of the recoverable
|
|
2848
|
+
* per-frame drawElement conditions (canvas-not-initialized / no-cached-paint-
|
|
2849
|
+
* record, #3423):
|
|
2850
|
+
* - Recoverable: capture the remaining frames directly via screenshot,
|
|
2851
|
+
* same as the per-frame paths' own fallback (avoids re-attempting a
|
|
2852
|
+
* drawElement produce that the batch call just told us will fail again —
|
|
2853
|
+
* review finding: audit this path explicitly rather than relying on the
|
|
2854
|
+
* incidental retry-then-catch behavior below).
|
|
2855
|
+
* - Anything else (unrecognized error): fall through to
|
|
2856
|
+
* {@link captureFrameToBufferPipelined}, which re-attempts drawElement (so
|
|
2857
|
+
* a genuinely transient, non-drawElement-specific failure still gets a
|
|
2858
|
+
* second chance) and owns the same recoverable-error/fatal-error split for
|
|
2859
|
+
* whatever it encounters — so failure behavior for a truly fatal error is
|
|
2860
|
+
* identical to the unbatched path, just discovered at batch granularity.
|
|
2801
2861
|
*/
|
|
2802
2862
|
export async function captureFramesBatchPipelined(session, frameIndices, times) {
|
|
2803
2863
|
const { page, options } = session;
|
|
@@ -2829,16 +2889,57 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
|
|
|
2829
2889
|
results.push({ frameIndex, encodeResult });
|
|
2830
2890
|
}
|
|
2831
2891
|
if (failedAt !== null) {
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2892
|
+
// `error` is a plain string here (produceDrawElementFrameBatch returns it
|
|
2893
|
+
// out of an in-page evaluate rather than throwing an Error instance) —
|
|
2894
|
+
// isRecoverableDrawElementError accepts `unknown` and stringifies non-Error
|
|
2895
|
+
// input, so passing the string straight through classifies it correctly,
|
|
2896
|
+
// including through produceDrawElementFrameBatch's own error text (which
|
|
2897
|
+
// embeds the same DE_CANVAS_NOT_INITIALIZED_CODE / native paint-record
|
|
2898
|
+
// phrase the per-frame paths match on).
|
|
2899
|
+
if (isRecoverableDrawElementError(error)) {
|
|
2900
|
+
const reason = isCanvasNotInitializedError(error)
|
|
2901
|
+
? "drawElement canvas not initialized"
|
|
2902
|
+
: "No cached paint record";
|
|
2903
|
+
console.log(`[engine] fast capture: batch produce failed at frame ` +
|
|
2904
|
+
`${frameIndices[failedAt] ?? "?"} (${reason}); ` +
|
|
2905
|
+
`screenshot fallback for ${frameIndices.length - failedAt} frame(s) ` +
|
|
2906
|
+
`(see fast-capture-limitations.md)`);
|
|
2907
|
+
for (let i = failedAt; i < frameIndices.length; i++) {
|
|
2908
|
+
const frameIndex = frameIndices[i];
|
|
2909
|
+
const time = times[i];
|
|
2910
|
+
if (frameIndex === undefined || time === undefined)
|
|
2911
|
+
break;
|
|
2912
|
+
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
|
2913
|
+
// Each remaining frame still needs its own seek/prepare — the batch
|
|
2914
|
+
// produce call left the page composited for whichever frame it last
|
|
2915
|
+
// attempted, not this one. Without this, every fallback screenshot in
|
|
2916
|
+
// the loop captures the SAME (stale) frame instead of advancing.
|
|
2917
|
+
// Deliberately reuse prepareFrameForCapture rather than routing
|
|
2918
|
+
// through captureFrameToBufferPipelined here, since that would
|
|
2919
|
+
// re-attempt produceDrawElementFrame — which the batch call already
|
|
2920
|
+
// told us will fail again for these frames (see function doc above).
|
|
2921
|
+
await prepareFrameForCapture(session, frameIndex, time);
|
|
2922
|
+
const buffer = await pageScreenshotCapture(page, options);
|
|
2923
|
+
const encodeResult = Promise.resolve(buffer);
|
|
2924
|
+
if (session.staticFrames) {
|
|
2925
|
+
session.lastEncodeResult = encodeResult;
|
|
2926
|
+
session.lastEncodeResultFrame = frameIndex;
|
|
2927
|
+
}
|
|
2928
|
+
results.push({ frameIndex, encodeResult });
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
else {
|
|
2932
|
+
console.log(`[engine] fast capture: batch produce failed at frame ` +
|
|
2933
|
+
`${frameIndices[failedAt] ?? "?"} (${error ?? "?"}); ` +
|
|
2934
|
+
`re-capturing ${frameIndices.length - failedAt} frame(s) per-frame`);
|
|
2935
|
+
for (let i = failedAt; i < frameIndices.length; i++) {
|
|
2936
|
+
const frameIndex = frameIndices[i];
|
|
2937
|
+
const time = times[i];
|
|
2938
|
+
if (frameIndex === undefined || time === undefined)
|
|
2939
|
+
break;
|
|
2940
|
+
const { encodeResult } = await captureFrameToBufferPipelined(session, frameIndex, time);
|
|
2941
|
+
results.push({ frameIndex, encodeResult });
|
|
2942
|
+
}
|
|
2842
2943
|
}
|
|
2843
2944
|
}
|
|
2844
2945
|
// Task B: retain the last encode result so a following static frame can reuse it.
|
|
@@ -3137,8 +3238,46 @@ export function percentileOf(samples, p) {
|
|
|
3137
3238
|
const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(p * sorted.length)));
|
|
3138
3239
|
return Math.round(sorted[idx] ?? 0);
|
|
3139
3240
|
}
|
|
3241
|
+
/**
|
|
3242
|
+
* Fraction of captured frames above which a fast-capture render is treated as
|
|
3243
|
+
* "drawElement effectively didn't engage" rather than "recovered a handful of
|
|
3244
|
+
* edge-case frames" (see the cross-PR-seam warning in
|
|
3245
|
+
* {@link getCapturePerfSummary}). Not currently a hard gate — see that
|
|
3246
|
+
* function's comment for why — just the threshold for the loud diagnostic.
|
|
3247
|
+
*/
|
|
3248
|
+
const DE_FALLBACK_RATIO_WARN_THRESHOLD = 0.5;
|
|
3140
3249
|
export function getCapturePerfSummary(session) {
|
|
3141
3250
|
const frames = Math.max(1, session.capturePerf.frames);
|
|
3251
|
+
const ncprFallbacks = session.deNcprFallbacks ?? 0;
|
|
3252
|
+
// Cross-PR seam (#3423 per-frame screenshot fallback vs #3429 artifact
|
|
3253
|
+
// validation): #3429's artifact validation only checks that the render
|
|
3254
|
+
// produced the right frame COUNT and duration — it has no visibility into
|
|
3255
|
+
// HOW each frame was captured. If a composition is so incompatible with
|
|
3256
|
+
// drawElement that most/all frames take the per-frame screenshot fallback
|
|
3257
|
+
// added here, the render still reports "complete" with a correct frame
|
|
3258
|
+
// count, even though drawElement effectively never engaged for it. That's
|
|
3259
|
+
// not itself a correctness bug — screenshot capture is the platform's
|
|
3260
|
+
// normal, well-tested baseline, so the SHIPPED PIXELS are fine — but a
|
|
3261
|
+
// near-100% fallback ratio is a strong signal that fast-capture silently
|
|
3262
|
+
// failed to engage for the whole render (e.g. a persistent canvas-injection
|
|
3263
|
+
// problem) rather than recovering a handful of expected edge-case frames,
|
|
3264
|
+
// and today nothing surfaces that distinction to telemetry or to a human.
|
|
3265
|
+
//
|
|
3266
|
+
// Deliberately NOT a circuit breaker: aborting/failing the render here
|
|
3267
|
+
// would make a render that reliably succeeds via the well-tested screenshot
|
|
3268
|
+
// path fail instead, which is a worse outcome than a slow-but-correct
|
|
3269
|
+
// render. Whether artifact validation (or this session) should eventually
|
|
3270
|
+
// gate on the ratio — and where that decision belongs — is tracked as an
|
|
3271
|
+
// explicit follow-up: https://github.com/heygen-com/hyperframes/issues/3482
|
|
3272
|
+
// ("Fast-capture: fallback-ratio guard for #3423 x #3429 seam"), rather
|
|
3273
|
+
// than decided unilaterally in this review-response commit.
|
|
3274
|
+
if (frames > 0 && ncprFallbacks / frames > DE_FALLBACK_RATIO_WARN_THRESHOLD) {
|
|
3275
|
+
const pct = Math.round((ncprFallbacks / frames) * 100);
|
|
3276
|
+
console.warn(`[engine] fast capture: ${ncprFallbacks}/${frames} frame(s) (${pct}%) fell back to ` +
|
|
3277
|
+
`screenshot capture (canvas-not-initialized / no-cached-paint-record) — ` +
|
|
3278
|
+
`drawElement likely failed to engage for this render rather than recovering a few ` +
|
|
3279
|
+
`edge-case frames; see fast-capture-limitations.md.`);
|
|
3280
|
+
}
|
|
3142
3281
|
return {
|
|
3143
3282
|
frames: session.capturePerf.frames,
|
|
3144
3283
|
avgTotalMs: Math.round(session.capturePerf.totalMs / frames),
|
|
@@ -3177,7 +3316,7 @@ export function getCapturePerfSummary(session) {
|
|
|
3177
3316
|
deVerifyArmed: session.deVerifyFrames?.size ?? 0,
|
|
3178
3317
|
deVerifyInitMs: session.deVerifyInitMs ?? 0,
|
|
3179
3318
|
deBoundaryFrames: session.clipBoundaryFrames?.size ?? 0,
|
|
3180
|
-
deNcprFallbacks:
|
|
3319
|
+
deNcprFallbacks: ncprFallbacks,
|
|
3181
3320
|
};
|
|
3182
3321
|
}
|
|
3183
3322
|
//# sourceMappingURL=frameCapture.js.map
|