@hyperframes/engine 0.7.59 → 0.7.61

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 (76) hide show
  1. package/README.md +3 -4
  2. package/dist/config.d.ts +95 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +131 -16
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +7 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +7 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/audioMixer.d.ts.map +1 -1
  11. package/dist/services/audioMixer.js +38 -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 +134 -177
  20. package/dist/services/browserManager.js.map +1 -1
  21. package/dist/services/chunkEncoder.d.ts.map +1 -1
  22. package/dist/services/chunkEncoder.js +45 -134
  23. package/dist/services/chunkEncoder.js.map +1 -1
  24. package/dist/services/frameCapture.d.ts +38 -1
  25. package/dist/services/frameCapture.d.ts.map +1 -1
  26. package/dist/services/frameCapture.js +119 -8
  27. package/dist/services/frameCapture.js.map +1 -1
  28. package/dist/services/pageNavigationTimeoutErrorHint.d.ts +79 -0
  29. package/dist/services/pageNavigationTimeoutErrorHint.d.ts.map +1 -0
  30. package/dist/services/pageNavigationTimeoutErrorHint.js +112 -0
  31. package/dist/services/pageNavigationTimeoutErrorHint.js.map +1 -0
  32. package/dist/services/parallelCoordinator.d.ts +21 -0
  33. package/dist/services/parallelCoordinator.d.ts.map +1 -1
  34. package/dist/services/parallelCoordinator.js +40 -1
  35. package/dist/services/parallelCoordinator.js.map +1 -1
  36. package/dist/services/protocolTimeoutErrorHint.d.ts +28 -0
  37. package/dist/services/protocolTimeoutErrorHint.d.ts.map +1 -0
  38. package/dist/services/protocolTimeoutErrorHint.js +45 -0
  39. package/dist/services/protocolTimeoutErrorHint.js.map +1 -0
  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/systemMemory.d.ts.map +1 -1
  44. package/dist/services/systemMemory.js +4 -1
  45. package/dist/services/systemMemory.js.map +1 -1
  46. package/dist/services/videoFrameExtractor.d.ts +4 -5
  47. package/dist/services/videoFrameExtractor.d.ts.map +1 -1
  48. package/dist/services/videoFrameExtractor.js +57 -112
  49. package/dist/services/videoFrameExtractor.js.map +1 -1
  50. package/dist/types.d.ts +28 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/utils/ffmpegBinaries.d.ts +2 -2
  53. package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
  54. package/dist/utils/ffmpegBinaries.js +8 -82
  55. package/dist/utils/ffmpegBinaries.js.map +1 -1
  56. package/dist/utils/ffprobe.d.ts +3 -1
  57. package/dist/utils/ffprobe.d.ts.map +1 -1
  58. package/dist/utils/ffprobe.js +35 -42
  59. package/dist/utils/ffprobe.js.map +1 -1
  60. package/dist/utils/gpuEncoder.d.ts.map +1 -1
  61. package/dist/utils/gpuEncoder.js +32 -52
  62. package/dist/utils/gpuEncoder.js.map +1 -1
  63. package/dist/utils/gpuParityDiff.d.ts +142 -0
  64. package/dist/utils/gpuParityDiff.d.ts.map +1 -0
  65. package/dist/utils/gpuParityDiff.js +205 -0
  66. package/dist/utils/gpuParityDiff.js.map +1 -0
  67. package/dist/utils/managedChildProcess.d.ts +50 -0
  68. package/dist/utils/managedChildProcess.d.ts.map +1 -0
  69. package/dist/utils/managedChildProcess.js +148 -0
  70. package/dist/utils/managedChildProcess.js.map +1 -0
  71. package/dist/utils/processTracker.js +1 -1
  72. package/dist/utils/runFfmpeg.d.ts +3 -0
  73. package/dist/utils/runFfmpeg.d.ts.map +1 -1
  74. package/dist/utils/runFfmpeg.js +16 -50
  75. package/dist/utils/runFfmpeg.js.map +1 -1
  76. package/package.json +3 -2
@@ -237,6 +237,23 @@ export function formatRequestFailureDiagnostic(input) {
237
237
  return (`[Browser:REQUESTFAILED] ${input.method} ${sanitizeDiagnosticUrl(input.url)} ` +
238
238
  `resource=${input.resourceType} error=${input.failureText}`);
239
239
  }
240
+ /**
241
+ * Chromium reports media loads that it intentionally cancels during probing as
242
+ * request failures. They are expected when the probe discovers or seeks local
243
+ * audio/video and do not indicate a missing asset.
244
+ */
245
+ export function shouldIgnoreRequestFailureDiagnostic(input) {
246
+ if (input.failureText !== "net::ERR_ABORTED")
247
+ return false;
248
+ if (input.resourceType === "media")
249
+ return true;
250
+ try {
251
+ return /\.(?:aac|flac|m4a|mp3|mp4|mov|oga|ogg|ogv|wav|webm)$/i.test(new URL(input.url).pathname);
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ }
240
257
  export function formatHttpErrorDiagnostic(input) {
241
258
  const statusText = input.statusText ? ` ${input.statusText}` : "";
242
259
  return (`[Browser:HTTP${input.status}] ${input.method} ${sanitizeDiagnosticUrl(input.url)} ` +
@@ -347,11 +364,13 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
347
364
  (!forceScreenshot || forceDE);
348
365
  if ((session.config?.useDrawElement ?? false) && supersampling) {
349
366
  session.deGateReason = "supersampling";
367
+ session.deFallbackTrigger = "supersampling";
350
368
  console.log("[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
351
369
  "ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.");
352
370
  }
353
371
  if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
354
372
  session.deGateReason = "render_mode_hint";
373
+ session.deFallbackTrigger = "render_mode_hint";
355
374
  console.log("[engine] fast capture: falling back to screenshot — render-mode compatibility " +
356
375
  "hint forced screenshot capture (e.g. raw requestAnimationFrame composition).");
357
376
  }
@@ -397,6 +416,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
397
416
  });
398
417
  if (!supportsDrawElement) {
399
418
  session.deGateReason = "unsupported_chrome";
419
+ session.deFallbackTrigger = "unsupported_chrome";
400
420
  console.log(`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
401
421
  "this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only " +
402
422
  "feature, ~151+); run `hyperframes browser ensure --force` to fetch a supported " +
@@ -420,6 +440,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
420
440
  const mode = resolveDrawElementCaptureMode(session.isSwiftShader, transparent);
421
441
  if (mode === "screenshot") {
422
442
  session.deGateReason = "swiftshader";
443
+ session.deFallbackTrigger = "swiftshader";
423
444
  // Fall back to the browser's LAUNCH mode, not unconditionally to
424
445
  // "screenshot": on a BeginFrame-launched browser (Linux fast capture)
425
446
  // Page.captureScreenshot hangs for the full protocol timeout, while
@@ -439,6 +460,11 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
439
460
  const cssFx = await detectCssEffectRisk(page);
440
461
  if (cssFx) {
441
462
  session.deGateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
463
+ // Full specific effect ("filter:blur" / "filter:drop-shadow" /
464
+ // "backdrop-filter" / "clip-path") — `deGateReason` sanitizes
465
+ // this to the low-cardinality prefix; `deFallbackTrigger` keeps
466
+ // the fine-grained value for the diagnostic profile emission.
467
+ session.deFallbackTrigger = cssFx;
442
468
  console.log(`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
443
469
  `${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`);
444
470
  await routeToFallback();
@@ -469,6 +495,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
469
495
  logInitPhase(`timeline at-risk predictor: ${atRisk.size}/${totalFrames} frames (${Math.round(atRiskFraction * 100)}%)`);
470
496
  if (atRisk.size > 0 && atRiskFraction > fractionFloor) {
471
497
  session.deGateReason = "at_risk_timeline";
498
+ session.deFallbackTrigger = "at_risk_timeline";
472
499
  console.log(`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
473
500
  `${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop ` +
474
501
  `(blend/3D/clip/mask); drawElementImage drops these mid-animation ` +
@@ -484,6 +511,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
484
511
  const threeD = await initThreeDProjection(page);
485
512
  if (!forceDE && !threeD.ok) {
486
513
  session.deGateReason = "3d_init_failed";
514
+ session.deFallbackTrigger = "3d_init_failed";
487
515
  console.log(`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
488
516
  `3D projection init failed (${threeD.reason ?? "unknown"})`);
489
517
  await routeToFallback();
@@ -642,8 +670,56 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
642
670
  ? "beginframe"
643
671
  : "screenshot";
644
672
  const chromeArgs = buildChromeArgs({ width: options.width, height: options.height, captureMode: preMode }, { ...config, browserGpuMode: resolvedGpuMode });
645
- const { browser, captureMode } = await acquireBrowser(chromeArgs, config);
673
+ const browserLease = await acquireBrowser(chromeArgs, config);
674
+ return constructCaptureSessionWithRollback({
675
+ browserLease,
676
+ serverUrl,
677
+ outputDir,
678
+ options,
679
+ onBeforeCapture,
680
+ config,
681
+ useDrawElement,
682
+ });
683
+ }
684
+ async function constructCaptureSessionWithRollback(input) {
685
+ let page;
686
+ try {
687
+ return await constructCaptureSession({
688
+ ...input,
689
+ onPageCreated: (createdPage) => {
690
+ page = createdPage;
691
+ },
692
+ });
693
+ }
694
+ catch (error) {
695
+ let pageClosed = true;
696
+ try {
697
+ if (page) {
698
+ const rollbackPage = page;
699
+ pageClosed = await waitForCloseWithTimeout(Promise.resolve().then(() => rollbackPage.close()));
700
+ }
701
+ }
702
+ finally {
703
+ if (!pageClosed) {
704
+ console.warn("[FrameCapture] Timed out closing page during construction rollback; forcing browser process shutdown");
705
+ input.browserLease.forceRelease();
706
+ }
707
+ else {
708
+ const browserClosed = await waitForCloseWithTimeout(input.browserLease.release());
709
+ if (!browserClosed) {
710
+ console.warn("[FrameCapture] Timed out closing browser during construction rollback; forcing browser process shutdown");
711
+ input.browserLease.forceRelease();
712
+ }
713
+ }
714
+ }
715
+ throw error;
716
+ }
717
+ }
718
+ async function constructCaptureSession(input) {
719
+ const { browserLease, serverUrl, outputDir, options, onBeforeCapture, config, useDrawElement, onPageCreated, } = input;
720
+ const { browser, captureMode } = browserLease;
646
721
  const page = await browser.newPage();
722
+ onPageCreated(page);
647
723
  // Polyfill esbuild's keepNames helper inside the page.
648
724
  //
649
725
  // The engine is published as raw TypeScript (`packages/engine/package.json`
@@ -744,6 +820,7 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
744
820
  // PNG captures after `page.goto(...)` and the `window.__hf` readiness poll.
745
821
  return {
746
822
  browser,
823
+ browserLease,
747
824
  page,
748
825
  options: sessionOptions,
749
826
  serverUrl,
@@ -1260,14 +1337,19 @@ export async function initializeSession(session) {
1260
1337
  appendBrowserDiagnostic(session, text);
1261
1338
  });
1262
1339
  page.on("requestfailed", (request) => {
1263
- if (request.resourceType() === "script") {
1340
+ const resourceType = request.resourceType();
1341
+ const url = request.url();
1342
+ const failureText = request.failure()?.errorText ?? "unknown";
1343
+ if (resourceType === "script") {
1264
1344
  recordScriptLoadFailure(session, request.url());
1265
1345
  }
1346
+ if (shouldIgnoreRequestFailureDiagnostic({ resourceType, url, failureText }))
1347
+ return;
1266
1348
  appendBrowserDiagnostic(session, formatRequestFailureDiagnostic({
1267
1349
  method: request.method(),
1268
- resourceType: request.resourceType(),
1269
- url: request.url(),
1270
- failureText: request.failure()?.errorText ?? "unknown",
1350
+ resourceType,
1351
+ url,
1352
+ failureText,
1271
1353
  }));
1272
1354
  });
1273
1355
  page.on("response", (response) => {
@@ -2556,16 +2638,22 @@ export async function closeCaptureSession(session) {
2556
2638
  const pageClosed = await waitForCloseWithTimeout(session.page.close());
2557
2639
  if (!pageClosed) {
2558
2640
  console.warn("[FrameCapture] Timed out closing page; forcing browser process shutdown");
2559
- forceReleaseBrowser(session.browser);
2641
+ if (session.browserLease)
2642
+ session.browserLease.forceRelease();
2643
+ else
2644
+ forceReleaseBrowser(session.browser);
2560
2645
  session.browserReleased = true;
2561
2646
  }
2562
2647
  session.pageReleased = true;
2563
2648
  }
2564
2649
  if (!session.browserReleased && session.browser) {
2565
- const browserClosed = await waitForCloseWithTimeout(releaseBrowser(session.browser, session.config));
2650
+ const browserClosed = await waitForCloseWithTimeout(session.browserLease?.release() ?? releaseBrowser(session.browser, session.config));
2566
2651
  if (!browserClosed) {
2567
2652
  console.warn("[FrameCapture] Timed out closing browser; forcing browser process shutdown");
2568
- forceReleaseBrowser(session.browser);
2653
+ if (session.browserLease)
2654
+ session.browserLease.forceRelease();
2655
+ else
2656
+ forceReleaseBrowser(session.browser);
2569
2657
  }
2570
2658
  session.browserReleased = true;
2571
2659
  }
@@ -2729,6 +2817,26 @@ function medianOf(samples) {
2729
2817
  const sorted = [...samples].sort((a, b) => a - b);
2730
2818
  return Math.round(sorted[Math.floor(sorted.length / 2)] ?? 0);
2731
2819
  }
2820
+ /**
2821
+ * Percentile of a positive-real sample set (nearest-rank; matches how the
2822
+ * existing {@link medianOf} p50 helper picks the middle index). `p` is a
2823
+ * fraction in [0, 1]; the sample at `floor(p * n)` (clamped to `[0, n-1]`)
2824
+ * is returned. Sample set is not mutated. Returns 0 for empty input, mirroring
2825
+ * the p50 helper.
2826
+ *
2827
+ * Used for the `capture_fallback_profile` observability checkpoint added in
2828
+ * the fast-capture fallback profiling PR: we already collect `capturePerf.frameMs`
2829
+ * per session, so computing p95/p99 is one sort + two lookups — cheap enough
2830
+ * to always compute alongside the existing p50, no separate opt-in path
2831
+ * needed for the math. The env gate lives at the emission site.
2832
+ */
2833
+ export function percentileOf(samples, p) {
2834
+ if (samples.length === 0)
2835
+ return 0;
2836
+ const sorted = [...samples].sort((a, b) => a - b);
2837
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(p * sorted.length)));
2838
+ return Math.round(sorted[idx] ?? 0);
2839
+ }
2732
2840
  export function getCapturePerfSummary(session) {
2733
2841
  const frames = Math.max(1, session.capturePerf.frames);
2734
2842
  return {
@@ -2738,6 +2846,8 @@ export function getCapturePerfSummary(session) {
2738
2846
  avgBeforeCaptureMs: Math.round(session.capturePerf.beforeCaptureMs / frames),
2739
2847
  avgScreenshotMs: Math.round(session.capturePerf.screenshotMs / frames),
2740
2848
  p50TotalMs: medianOf(session.capturePerf.frameMs),
2849
+ p95TotalMs: percentileOf(session.capturePerf.frameMs, 0.95),
2850
+ p99TotalMs: percentileOf(session.capturePerf.frameMs, 0.99),
2741
2851
  subTimelineWaitOutcome: session.subTimelineWaitOutcome,
2742
2852
  warnings: session.warnings.map((warning) => ({
2743
2853
  ...warning,
@@ -2758,6 +2868,7 @@ export function getCapturePerfSummary(session) {
2758
2868
  beginFrameHasDamage: session.beginFrameHasDamageCount,
2759
2869
  captureMode: session.captureMode,
2760
2870
  deGateReason: session.deGateReason,
2871
+ deFallbackTrigger: session.deFallbackTrigger,
2761
2872
  deWorkerEncode: session.workerEncodeEnabled ?? false,
2762
2873
  deVerifyArmed: session.deVerifyFrames?.size ?? 0,
2763
2874
  deVerifyInitMs: session.deVerifyInitMs ?? 0,