@hyperframes/engine 0.8.198 → 0.8.199

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.
@@ -761,6 +761,8 @@ export declare function getCompositionDuration(session: CaptureSession): Promise
761
761
  * timeline passed verification while the drained output bottomed at 30.9 dB.
762
762
  */
763
763
  export declare function computeDeVerifySampleFractions(k: number): number[];
764
+ /** Seeks the page the truth is read from; callbacks run unless suppressed (suppressed, a count-up's text is stale). */
765
+ export declare function seekDeVerifyTruth(page: Page, t: number, suppressEvents: boolean): Promise<void>;
764
766
  /**
765
767
  * Percentile of a positive-real sample set (nearest-rank; matches how the
766
768
  * existing {@link medianOf} p50 helper picks the middle index). `p` is a
@@ -3774,6 +3774,14 @@ export function computeDeVerifySampleFractions(k) {
3774
3774
  return [0.95];
3775
3775
  return [...Array.from({ length: k - 1 }, (_, i) => (i + 1) / k), 0.95];
3776
3776
  }
3777
+ /** Seeks the page the truth is read from; callbacks run unless suppressed (suppressed, a count-up's text is stale). */
3778
+ export async function seekDeVerifyTruth(page, t, suppressEvents) {
3779
+ await page.evaluate((tt, suppress) => {
3780
+ const hf = window.__hf;
3781
+ if (hf && typeof hf.seek === "function")
3782
+ hf.seek(tt, { suppressEvents: suppress });
3783
+ }, t, suppressEvents);
3784
+ }
3777
3785
  async function captureDeVerificationFrames(session, page, logInitPhase) {
3778
3786
  // Explicit HF_DE_VERIFY wins; otherwise the session's own sample count
3779
3787
  // (raised by the parallel coordinator for multi-worker DE), then default 4.
@@ -3819,48 +3827,54 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
3819
3827
  // would share the corruption, PSNR would pass on the damaged output.
3820
3828
  // Seeking 0 → ascending reproduces the render's own seek order.
3821
3829
  const fractions = computeDeVerifySampleFractions(k);
3822
- const seekTo = async (t) => {
3823
- await page.evaluate((tt) => {
3824
- const hf = window.__hf;
3825
- if (hf && typeof hf.seek === "function")
3826
- hf.seek(tt, { suppressEvents: true });
3827
- }, t);
3828
- };
3829
- await seekTo(quantizeTimeToFrame(0, fps));
3830
- // Force one frame so lazy tween initialization paints at t=0 state.
3831
- await pageScreenshotCapture(page, session.options);
3830
+ // A video-free comp reads its truth on a disposable page: callbacks that run there
3831
+ // cannot leave the render's own page changed. The video injector is tied to the render page.
3832
+ const disposable = session.onBeforeCapture
3833
+ ? null
3834
+ : await createStaticVerificationPage(session).catch(() => {
3835
+ logInitPhase("self-verify truth page unavailable; reading it on the render page");
3836
+ return null;
3837
+ });
3838
+ const isolated = disposable !== null;
3839
+ const truthPage = disposable ?? page;
3840
+ const seekTo = (t) => seekDeVerifyTruth(truthPage, t, !isolated);
3832
3841
  const frames = new Map();
3833
- for (const f of fractions) {
3834
- let idx = Math.min(totalFrames - 1, Math.max(1, Math.round(totalFrames * f)));
3835
- // Nudge off clip-cut boundaries (±1-frame desync is legitimate there);
3836
- // if the nudge saturates on a boundary index, skip the sample entirely.
3837
- let guard = 0;
3838
- while (boundary.has(idx) && guard++ < 6)
3839
- idx = Math.min(totalFrames - 1, idx + 2);
3840
- if (boundary.has(idx))
3841
- continue;
3842
- if (frames.has(idx))
3843
- continue;
3844
- const t = quantizeTimeToFrame(idx / fps, fps);
3845
- await seekTo(t);
3846
- // Video frame injection (same hook the real capture paths run) — without
3847
- // it, <video> elements screenshot black and every video comp would
3848
- // false-positive into the screenshot fallback.
3849
- if (session.onBeforeCapture)
3850
- await session.onBeforeCapture(page, t);
3851
- // Double-capture: the first screenshot forces a frame, which is what runs
3852
- // rAF-driven callbacks (count-up text counters land a tick after seek()
3853
- // returns — a single immediate screenshot captures stale text and
3854
- // false-positives the verify: 3bea8c73 28.7dB vs a truth missing its stat
3855
- // values while the DE frame was correct). NOTE: waiting on rAF via
3856
- // evaluate instead deadlocks — headless only fires rAF when a frame is
3857
- // produced, and nothing produces one until a screenshot asks.
3858
- await pageScreenshotCapture(page, session.options);
3859
- frames.set(idx, await pageScreenshotCapture(page, session.options));
3860
- }
3861
- // Leave the page at frame 0 so the render's first seek starts from the
3862
- // same state as an unverified render.
3863
- await seekTo(quantizeTimeToFrame(0, fps));
3842
+ try {
3843
+ await seekTo(quantizeTimeToFrame(0, fps));
3844
+ // Force one frame so lazy tween initialization paints at t=0 state.
3845
+ await pageScreenshotCapture(truthPage, session.options);
3846
+ for (const f of fractions) {
3847
+ let idx = Math.min(totalFrames - 1, Math.max(1, Math.round(totalFrames * f)));
3848
+ // Nudge off clip-cut boundaries (±1-frame desync is legitimate there);
3849
+ // if the nudge saturates on a boundary index, skip the sample entirely.
3850
+ let guard = 0;
3851
+ while (boundary.has(idx) && guard++ < 6)
3852
+ idx = Math.min(totalFrames - 1, idx + 2);
3853
+ if (boundary.has(idx))
3854
+ continue;
3855
+ if (frames.has(idx))
3856
+ continue;
3857
+ const t = quantizeTimeToFrame(idx / fps, fps);
3858
+ await seekTo(t);
3859
+ // Video frame injection (same hook the real capture paths run) — without
3860
+ // it, <video> elements screenshot black and every video comp would
3861
+ // false-positive into the screenshot fallback.
3862
+ if (session.onBeforeCapture)
3863
+ await session.onBeforeCapture(truthPage, t);
3864
+ // The first screenshot forces a frame, which runs rAF-driven callbacks (a count-up lands
3865
+ // a tick after seek()); waiting on rAF via evaluate deadlocks, headless fires it only
3866
+ // when a frame is produced.
3867
+ await pageScreenshotCapture(truthPage, session.options);
3868
+ frames.set(idx, await pageScreenshotCapture(truthPage, session.options));
3869
+ }
3870
+ // The render's own page goes back to frame 0 so its first seek starts like an unverified render.
3871
+ if (!isolated)
3872
+ await seekTo(quantizeTimeToFrame(0, fps));
3873
+ }
3874
+ finally {
3875
+ if (isolated)
3876
+ await truthPage.close().catch(() => { });
3877
+ }
3864
3878
  session.deVerifyFrames = frames;
3865
3879
  logInitPhase(`drawElement self-verify armed: ${frames.size} ground-truth frame(s) @ [${[...frames.keys()].join(", ")}] of ${totalFrames}`);
3866
3880
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.198",
3
+ "version": "0.8.199",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,8 +50,8 @@
50
50
  "linkedom": "^0.18.12",
51
51
  "puppeteer": "^25.8.0",
52
52
  "puppeteer-core": "^25.8.0",
53
- "@hyperframes/parsers": "^0.8.198",
54
- "@hyperframes/core": "^0.8.198"
53
+ "@hyperframes/core": "^0.8.199",
54
+ "@hyperframes/parsers": "^0.8.199"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",