@hyperframes/engine 0.7.60 → 0.7.62

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.
Files changed (66) hide show
  1. package/README.md +3 -4
  2. package/dist/config.d.ts +53 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +208 -16
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/audioMixer.d.ts.map +1 -1
  11. package/dist/services/audioMixer.js +19 -3
  12. package/dist/services/audioMixer.js.map +1 -1
  13. package/dist/services/browserLeasePool.d.ts +48 -0
  14. package/dist/services/browserLeasePool.d.ts.map +1 -0
  15. package/dist/services/browserLeasePool.js +210 -0
  16. package/dist/services/browserLeasePool.js.map +1 -0
  17. package/dist/services/browserManager.d.ts +6 -7
  18. package/dist/services/browserManager.d.ts.map +1 -1
  19. package/dist/services/browserManager.js +122 -177
  20. package/dist/services/browserManager.js.map +1 -1
  21. package/dist/services/captureFailure.d.ts +27 -0
  22. package/dist/services/captureFailure.d.ts.map +1 -0
  23. package/dist/services/captureFailure.js +134 -0
  24. package/dist/services/captureFailure.js.map +1 -0
  25. package/dist/services/chunkEncoder.d.ts.map +1 -1
  26. package/dist/services/chunkEncoder.js +45 -134
  27. package/dist/services/chunkEncoder.js.map +1 -1
  28. package/dist/services/frameCapture.d.ts +14 -3
  29. package/dist/services/frameCapture.d.ts.map +1 -1
  30. package/dist/services/frameCapture.js +100 -103
  31. package/dist/services/frameCapture.js.map +1 -1
  32. package/dist/services/parallelCoordinator.d.ts +2 -0
  33. package/dist/services/parallelCoordinator.d.ts.map +1 -1
  34. package/dist/services/parallelCoordinator.js +37 -6
  35. package/dist/services/parallelCoordinator.js.map +1 -1
  36. package/dist/services/screenshotService.d.ts +16 -0
  37. package/dist/services/screenshotService.d.ts.map +1 -1
  38. package/dist/services/screenshotService.js +20 -0
  39. package/dist/services/screenshotService.js.map +1 -1
  40. package/dist/services/streamingEncoder.d.ts.map +1 -1
  41. package/dist/services/streamingEncoder.js +23 -58
  42. package/dist/services/streamingEncoder.js.map +1 -1
  43. package/dist/services/videoFrameExtractor.d.ts.map +1 -1
  44. package/dist/services/videoFrameExtractor.js +39 -71
  45. package/dist/services/videoFrameExtractor.js.map +1 -1
  46. package/dist/utils/ffmpegBinaries.d.ts +2 -2
  47. package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
  48. package/dist/utils/ffmpegBinaries.js +8 -82
  49. package/dist/utils/ffmpegBinaries.js.map +1 -1
  50. package/dist/utils/ffprobe.d.ts +3 -1
  51. package/dist/utils/ffprobe.d.ts.map +1 -1
  52. package/dist/utils/ffprobe.js +35 -42
  53. package/dist/utils/ffprobe.js.map +1 -1
  54. package/dist/utils/gpuEncoder.d.ts.map +1 -1
  55. package/dist/utils/gpuEncoder.js +32 -52
  56. package/dist/utils/gpuEncoder.js.map +1 -1
  57. package/dist/utils/managedChildProcess.d.ts +50 -0
  58. package/dist/utils/managedChildProcess.d.ts.map +1 -0
  59. package/dist/utils/managedChildProcess.js +148 -0
  60. package/dist/utils/managedChildProcess.js.map +1 -0
  61. package/dist/utils/processTracker.js +1 -1
  62. package/dist/utils/runFfmpeg.d.ts +3 -0
  63. package/dist/utils/runFfmpeg.d.ts.map +1 -1
  64. package/dist/utils/runFfmpeg.js +16 -50
  65. package/dist/utils/runFfmpeg.js.map +1 -1
  66. package/package.json +3 -2
@@ -12,10 +12,11 @@ import { join } from "path";
12
12
  import { quantizeTimeToFrame, fpsToNumber } from "@hyperframes/core";
13
13
  // ── Extracted modules ───────────────────────────────────────────────────────
14
14
  import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, resolveHeadlessShellPath, } from "./browserManager.js";
15
- import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
15
+ import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
16
16
  import { detectSwiftShader, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, } from "./drawElementService.js";
17
17
  import { initThreeDProjection, detectCssEffectRisk } from "./threeDProjection.js";
18
18
  import { DEFAULT_CONFIG, applyConcreteGpuScreenshotClamp } from "../config.js";
19
+ export { isMemoryExhaustionError, isTransientBrowserError } from "./captureFailure.js";
19
20
  export class DrawElementVerificationError extends Error {
20
21
  kind;
21
22
  frameIndex;
@@ -237,6 +238,23 @@ export function formatRequestFailureDiagnostic(input) {
237
238
  return (`[Browser:REQUESTFAILED] ${input.method} ${sanitizeDiagnosticUrl(input.url)} ` +
238
239
  `resource=${input.resourceType} error=${input.failureText}`);
239
240
  }
241
+ /**
242
+ * Chromium reports media loads that it intentionally cancels during probing as
243
+ * request failures. They are expected when the probe discovers or seeks local
244
+ * audio/video and do not indicate a missing asset.
245
+ */
246
+ export function shouldIgnoreRequestFailureDiagnostic(input) {
247
+ if (input.failureText !== "net::ERR_ABORTED")
248
+ return false;
249
+ if (input.resourceType === "media")
250
+ return true;
251
+ try {
252
+ return /\.(?:aac|flac|m4a|mp3|mp4|mov|oga|ogg|ogv|wav|webm)$/i.test(new URL(input.url).pathname);
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
240
258
  export function formatHttpErrorDiagnostic(input) {
241
259
  const statusText = input.statusText ? ` ${input.statusText}` : "";
242
260
  return (`[Browser:HTTP${input.status}] ${input.method} ${sanitizeDiagnosticUrl(input.url)} ` +
@@ -653,8 +671,56 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
653
671
  ? "beginframe"
654
672
  : "screenshot";
655
673
  const chromeArgs = buildChromeArgs({ width: options.width, height: options.height, captureMode: preMode }, { ...config, browserGpuMode: resolvedGpuMode });
656
- const { browser, captureMode } = await acquireBrowser(chromeArgs, config);
674
+ const browserLease = await acquireBrowser(chromeArgs, config);
675
+ return constructCaptureSessionWithRollback({
676
+ browserLease,
677
+ serverUrl,
678
+ outputDir,
679
+ options,
680
+ onBeforeCapture,
681
+ config,
682
+ useDrawElement,
683
+ });
684
+ }
685
+ async function constructCaptureSessionWithRollback(input) {
686
+ let page;
687
+ try {
688
+ return await constructCaptureSession({
689
+ ...input,
690
+ onPageCreated: (createdPage) => {
691
+ page = createdPage;
692
+ },
693
+ });
694
+ }
695
+ catch (error) {
696
+ let pageClosed = true;
697
+ try {
698
+ if (page) {
699
+ const rollbackPage = page;
700
+ pageClosed = await waitForCloseWithTimeout(Promise.resolve().then(() => rollbackPage.close()));
701
+ }
702
+ }
703
+ finally {
704
+ if (!pageClosed) {
705
+ console.warn("[FrameCapture] Timed out closing page during construction rollback; forcing browser process shutdown");
706
+ input.browserLease.forceRelease();
707
+ }
708
+ else {
709
+ const browserClosed = await waitForCloseWithTimeout(input.browserLease.release());
710
+ if (!browserClosed) {
711
+ console.warn("[FrameCapture] Timed out closing browser during construction rollback; forcing browser process shutdown");
712
+ input.browserLease.forceRelease();
713
+ }
714
+ }
715
+ }
716
+ throw error;
717
+ }
718
+ }
719
+ async function constructCaptureSession(input) {
720
+ const { browserLease, serverUrl, outputDir, options, onBeforeCapture, config, useDrawElement, onPageCreated, } = input;
721
+ const { browser, captureMode } = browserLease;
657
722
  const page = await browser.newPage();
723
+ onPageCreated(page);
658
724
  // Polyfill esbuild's keepNames helper inside the page.
659
725
  //
660
726
  // The engine is published as raw TypeScript (`packages/engine/package.json`
@@ -755,6 +821,7 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
755
821
  // PNG captures after `page.goto(...)` and the `window.__hf` readiness poll.
756
822
  return {
757
823
  browser,
824
+ browserLease,
758
825
  page,
759
826
  options: sessionOptions,
760
827
  serverUrl,
@@ -1271,14 +1338,19 @@ export async function initializeSession(session) {
1271
1338
  appendBrowserDiagnostic(session, text);
1272
1339
  });
1273
1340
  page.on("requestfailed", (request) => {
1274
- if (request.resourceType() === "script") {
1341
+ const resourceType = request.resourceType();
1342
+ const url = request.url();
1343
+ const failureText = request.failure()?.errorText ?? "unknown";
1344
+ if (resourceType === "script") {
1275
1345
  recordScriptLoadFailure(session, request.url());
1276
1346
  }
1347
+ if (shouldIgnoreRequestFailureDiagnostic({ resourceType, url, failureText }))
1348
+ return;
1277
1349
  appendBrowserDiagnostic(session, formatRequestFailureDiagnostic({
1278
1350
  method: request.method(),
1279
- resourceType: request.resourceType(),
1280
- url: request.url(),
1281
- failureText: request.failure()?.errorText ?? "unknown",
1351
+ resourceType,
1352
+ url,
1353
+ failureText,
1282
1354
  }));
1283
1355
  });
1284
1356
  page.on("response", (response) => {
@@ -1386,6 +1458,19 @@ export async function initializeSession(session) {
1386
1458
  }
1387
1459
  recordCaptureWarnings(session, await collectMediaReadinessWarnings(page, skipVideoIds, pageReadyTimeout));
1388
1460
  await recordSessionInitTelemetry(session, initStart);
1461
+ // Ground-truth-check the upstream captureBeyondViewport request (see
1462
+ // pageContentExceedsCaptureHeight) now that the page is fully settled —
1463
+ // downgrade it when the page doesn't actually overflow the requested
1464
+ // capture height, since the beyond-viewport CDP path is otherwise pure
1465
+ // downside (HF#2550: phantom duplicate content on SwiftShader) for
1466
+ // content it was never needed for.
1467
+ if (session.options.captureBeyondViewport) {
1468
+ const needsBeyondViewport = await pageContentExceedsCaptureHeight(page, session.options.height);
1469
+ if (!needsBeyondViewport) {
1470
+ session.options.captureBeyondViewport = false;
1471
+ logInitPhase("captureBeyondViewport downgraded: page content fits the capture viewport");
1472
+ }
1473
+ }
1389
1474
  // drawElement or transparent-background init — runs after page is fully ready.
1390
1475
  await initDrawElementOrTransparentBackground(session, page, logInitPhase);
1391
1476
  await armStaticDedup(session, session.page, logInitPhase);
@@ -2567,16 +2652,22 @@ export async function closeCaptureSession(session) {
2567
2652
  const pageClosed = await waitForCloseWithTimeout(session.page.close());
2568
2653
  if (!pageClosed) {
2569
2654
  console.warn("[FrameCapture] Timed out closing page; forcing browser process shutdown");
2570
- forceReleaseBrowser(session.browser);
2655
+ if (session.browserLease)
2656
+ session.browserLease.forceRelease();
2657
+ else
2658
+ forceReleaseBrowser(session.browser);
2571
2659
  session.browserReleased = true;
2572
2660
  }
2573
2661
  session.pageReleased = true;
2574
2662
  }
2575
2663
  if (!session.browserReleased && session.browser) {
2576
- const browserClosed = await waitForCloseWithTimeout(releaseBrowser(session.browser, session.config));
2664
+ const browserClosed = await waitForCloseWithTimeout(session.browserLease?.release() ?? releaseBrowser(session.browser, session.config));
2577
2665
  if (!browserClosed) {
2578
2666
  console.warn("[FrameCapture] Timed out closing browser; forcing browser process shutdown");
2579
- forceReleaseBrowser(session.browser);
2667
+ if (session.browserLease)
2668
+ session.browserLease.forceRelease();
2669
+ else
2670
+ forceReleaseBrowser(session.browser);
2580
2671
  }
2581
2672
  session.browserReleased = true;
2582
2673
  }
@@ -2799,98 +2890,4 @@ export function getCapturePerfSummary(session) {
2799
2890
  deNcprFallbacks: session.deNcprFallbacks ?? 0,
2800
2891
  };
2801
2892
  }
2802
- // ── Transient browser error classification ─────────────────────────────────
2803
- // Puppeteer/Chrome can fail with transient errors that succeed on retry with a
2804
- // fresh browser session. These are infrastructure-level failures (frame
2805
- // detachment, connection drop, OOM kill, launch failure) — NOT composition bugs.
2806
- const TRANSIENT_BROWSER_ERROR_PATTERNS = [
2807
- /Navigating frame was detached/i,
2808
- /Target closed/i,
2809
- /Session closed/i,
2810
- /browser has disconnected/i,
2811
- /Page crashed/i,
2812
- /Execution context was destroyed/i,
2813
- /Cannot find context with specified id/i,
2814
- /Failed to launch the browser process/i,
2815
- /Navigation timeout of \d+ ms exceeded/i,
2816
- /ECONNREFUSED/i,
2817
- // Chromium can briefly invalidate even a localhost connection when Windows
2818
- // reports an adapter/route change. A fresh capture session succeeds once the
2819
- // network stack settles, so treat this like the other bounded navigation
2820
- // retries instead of failing the render immediately.
2821
- /net::ERR_NETWORK_CHANGED/i,
2822
- // pollHfReady's own timeout — thrown when window.__renderReady never flips
2823
- // true within playerReadyTimeout. "Runtime ready: false" means init simply
2824
- // didn't finish in time (commonly a slow/contended host, e.g. several
2825
- // concurrent renders), which a fresh session usually clears on retry. This
2826
- // is distinct from the "Runtime ready: true" fast-fail case a few lines up
2827
- // in pollHfReady (no timeline + no data-duration) — that's a genuine
2828
- // authoring bug and intentionally NOT matched here, so it still fails fast.
2829
- /Composition has zero duration[\s\S]*Runtime ready: false/,
2830
- ];
2831
- export function isTransientBrowserError(error) {
2832
- const message = error instanceof Error ? error.message : String(error);
2833
- return TRANSIENT_BROWSER_ERROR_PATTERNS.some((pattern) => pattern.test(message));
2834
- }
2835
- // ── Memory-exhaustion classification ────────────────────────────────────────
2836
- // A render can run the Node process (or a page-side allocation) out of memory
2837
- // on an oversized composition — huge canvas, thousands of frames, or a very
2838
- // large frame cache. These surface as cryptic V8 RangeErrors ("Set maximum
2839
- // size exceeded", "Invalid array length"/"string length", "Array buffer
2840
- // allocation failed") or a hard V8 heap-limit abort. They are NOT transient
2841
- // (a retry re-hits the same ceiling) and NOT composition-logic bugs — they're
2842
- // resource limits. Classify them so the caller can surface actionable guidance
2843
- // (lower resolution / fps / duration, or enable low-memory mode) instead of a
2844
- // raw RangeError.
2845
- // Deliberately specific: each pattern is a distinct V8/Node allocation-failure
2846
- // signature. We intentionally do NOT match a bare /out of memory/ — that
2847
- // substring appears in benign browser-console noise (WebGL `CONTEXT_LOST … out
2848
- // of memory`, GPU driver notes) that gets carried into the error path, and
2849
- // misclassifying it would replace the real failure message with generic OOM
2850
- // guidance.
2851
- const MEMORY_EXHAUSTION_ERROR_PATTERNS = [
2852
- /Set maximum size exceeded/i,
2853
- /Map maximum size exceeded/i,
2854
- /Invalid (?:array|string) length/i,
2855
- /Array buffer allocation failed/i,
2856
- /Cannot create a string longer than/i,
2857
- /Reached heap limit/i,
2858
- /JavaScript heap out of memory/i,
2859
- ];
2860
- // The producer's deployed runtime is Bun (JavaScriptCore), not Node (V8) —
2861
- // see `packages/gcp-cloud-run/Dockerfile`'s `CMD ["bun", "dist/server.js"]`.
2862
- // JSC's own allocation-failure message for the equivalent single-oversized-
2863
- // allocation RangeErrors above is the bare string "Out of memory" (verified:
2864
- // `new Uint8Array(Number.MAX_SAFE_INTEGER)`, an unbounded `Set`, and
2865
- // `"x".repeat(2**53)` all throw exactly this under Bun) — none of the V8
2866
- // patterns above match it. This is exactly the substring the comment above
2867
- // says NOT to match anywhere in the message (benign browser-console noise
2868
- // like a WebGL `CONTEXT_LOST … out of memory` carries that phrase too), so
2869
- // this checks the ENTIRE (trimmed) message equals it, not merely contains
2870
- // it — a compound message with other text around the phrase still misses.
2871
- const BUN_MEMORY_EXHAUSTION_EXACT_MESSAGE = /^out of memory\.?$/i;
2872
- // The parallel-DE capture path — the exact cohort the OOM-aware retry in
2873
- // renderOrchestrator.ts targets — never reaches isMemoryExhaustionError with
2874
- // a bare message: `executeParallelCapture`/`formatWorkerFailure`
2875
- // (parallelCoordinator.ts) always wrap a worker's error as
2876
- // "Worker N: <message>", optionally suffixed "; diagnostics: ..." and joined
2877
- // with other failed workers' segments via "; ", all prefixed
2878
- // "[Parallel] Capture failed: ". The exact-match check above is defeated by
2879
- // that wrapping entirely (verified) — this pattern recovers the Bun OOM
2880
- // signal by requiring "out of memory" appear immediately after "Worker N: "
2881
- // and immediately before end-of-string, ";", or ".", i.e. as the WHOLE
2882
- // worker-segment content, not merely somewhere inside it. This preserves the
2883
- // exact-match property (no bare "out of memory" substring inside otherwise-
2884
- // unrelated worker text, e.g. "Worker 2: WebGL context lost, out of memory
2885
- // reported by driver" does NOT match) while surviving this codebase's own
2886
- // error-flattening.
2887
- const BUN_MEMORY_EXHAUSTION_WRAPPED_WORKER_MESSAGE = /\bworker \d+: out of memory\.?(?:;|$)/i;
2888
- export function isMemoryExhaustionError(error) {
2889
- const message = error instanceof Error ? error.message : String(error);
2890
- if (BUN_MEMORY_EXHAUSTION_EXACT_MESSAGE.test(message.trim()))
2891
- return true;
2892
- if (BUN_MEMORY_EXHAUSTION_WRAPPED_WORKER_MESSAGE.test(message))
2893
- return true;
2894
- return MEMORY_EXHAUSTION_ERROR_PATTERNS.some((pattern) => pattern.test(message));
2895
- }
2896
2893
  //# sourceMappingURL=frameCapture.js.map