@hyperframes/engine 0.8.197 → 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.
package/dist/index.d.ts CHANGED
@@ -38,7 +38,7 @@ export { chromeMajorCeiling } from "./services/chromeHostCeiling.js";
38
38
  export { acquireBrowser, setHostHandlesSigint, releaseBrowser, drainBrowserPool, closeBrowserPool, resolveHeadlessShellPath, resolveBrowserGpuMode, buildChromeArgs, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, WebGpuUnavailableError, ENABLE_BROWSER_POOL, BrowserLeasePool, type GpuProbeLifecycle, type BuildChromeArgsOptions, type BrowserLaunchFingerprint, type BrowserLease, type BrowserPoolState, type CaptureMode, type AcquiredBrowser, } from "./services/browserManager.js";
39
39
  export { augmentProtocolTimeoutError, isProtocolTimeoutError, } from "./services/protocolTimeoutErrorHint.js";
40
40
  export { augmentPageNavigationTimeoutError, isPageNavigationTimeoutError, type NavigationTimeoutHintContext, } from "./services/pageNavigationTimeoutErrorHint.js";
41
- export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, type DrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, type CaptureSession, isTransientBrowserError, isMemoryExhaustionError, type BeforeCaptureHook, type DiscardWarmupInnerCapture, type StaticVerificationOutcome, VfxFailureError, waitForImagesReady, } from "./services/frameCapture.js";
41
+ export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, type DrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, type CaptureSession, isTransientBrowserError, isMemoryExhaustionError, type BeforeCaptureHook, type DiscardWarmupInnerCapture, type StaticVerificationOutcome, VfxFailureError, waitForImagesReady, evaluateHfDiagnostic, buildZeroDurationDiagnostic, } from "./services/frameCapture.js";
42
42
  export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, type CaptureFailureKind, type CaptureWorkerDiagnostic, } from "./services/captureFailure.js";
43
43
  export { createChromeMemorySampler, type ChromeMemoryStats, } from "./services/chromeMemorySampler.js";
44
44
  export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
@@ -46,7 +46,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
46
46
  export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
47
47
  export type { EncoderOptions, EncodeResult, MuxResult } from "./services/chunkEncoder.types.js";
48
48
  export { spawnStreamingEncoder, createFrameReorderBuffer, type StreamingEncoder, type StreamingEncoderOptions, type StreamingEncoderResult, type FrameReorderBuffer, } from "./services/streamingEncoder.js";
49
- export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
49
+ export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
50
50
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
51
51
  export { resolveReferencedStart, resolveReferencedDuration, type RefResolverEl, type RefResolverDoc, } from "./services/referenceResolver.js";
52
52
  export { createVideoFrameInjector } from "./services/videoFrameInjector.js";
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ export { acquireBrowser, setHostHandlesSigint, releaseBrowser, drainBrowserPool,
40
40
  export { augmentProtocolTimeoutError, isProtocolTimeoutError, } from "./services/protocolTimeoutErrorHint.js";
41
41
  export { augmentPageNavigationTimeoutError, isPageNavigationTimeoutError, } from "./services/pageNavigationTimeoutErrorHint.js";
42
42
  // ── Frame capture pipeline ──────────────────────────────────────────────────────
43
- export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, isTransientBrowserError, isMemoryExhaustionError, VfxFailureError, waitForImagesReady, } from "./services/frameCapture.js";
43
+ export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, isTransientBrowserError, isMemoryExhaustionError, VfxFailureError, waitForImagesReady, evaluateHfDiagnostic, buildZeroDurationDiagnostic, } from "./services/frameCapture.js";
44
44
  export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, } from "./services/captureFailure.js";
45
45
  export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
46
46
  // ── Screenshot (BeginFrame) ─────────────────────────────────────────────────────
@@ -50,7 +50,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
50
50
  export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
51
51
  export { spawnStreamingEncoder, createFrameReorderBuffer, } from "./services/streamingEncoder.js";
52
52
  // ── Media processing ───────────────────────────────────────────────────────────
53
- export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
53
+ export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
54
54
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
55
55
  export { resolveReferencedStart, resolveReferencedDuration, } from "./services/referenceResolver.js";
56
56
  export { createVideoFrameInjector } from "./services/videoFrameInjector.js";
@@ -410,6 +410,7 @@ export interface HfDiagnostic {
410
410
  rejectedBuildReadyKeys: string[];
411
411
  }
412
412
  export declare function buildZeroDurationDiagnostic(diag: HfDiagnostic): string;
413
+ export declare function evaluateHfDiagnostic(page: Page): Promise<HfDiagnostic>;
413
414
  export declare function pollHfReady(page: Page, timeoutMs: number, intervalMs?: number): Promise<void>;
414
415
  export declare function waitForSubCompositionTimelines(session: CaptureSession, page: Page, timeoutMs: number, options?: {
415
416
  scriptErrorGraceMs?: number;
@@ -760,6 +761,8 @@ export declare function getCompositionDuration(session: CaptureSession): Promise
760
761
  * timeline passed verification while the drained output bottomed at 30.9 dB.
761
762
  */
762
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>;
763
766
  /**
764
767
  * Percentile of a positive-real sample set (nearest-rank; matches how the
765
768
  * existing {@link medianOf} p50 helper picks the middle index). `p` is a
@@ -1131,7 +1131,7 @@ export function buildZeroDurationDiagnostic(diag) {
1131
1131
  `data-duration: ${diag.declaredDuration > 0 ? diag.declaredDuration + "s" : "not set"}\n` +
1132
1132
  (hints.length > 0 ? hints.map((h) => ` → ${h}`).join("\n") : ""));
1133
1133
  }
1134
- async function evaluateHfDiagnostic(page) {
1134
+ export async function evaluateHfDiagnostic(page) {
1135
1135
  return (await page.evaluate(HF_READY_DIAGNOSTIC_EXPR));
1136
1136
  }
1137
1137
  export async function pollHfReady(page, timeoutMs, intervalMs = 100) {
@@ -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.197",
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.197",
54
- "@hyperframes/core": "^0.8.197"
53
+ "@hyperframes/core": "^0.8.199",
54
+ "@hyperframes/parsers": "^0.8.199"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",