@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.
- package/README.md +3 -4
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +208 -16
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +19 -3
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/browserLeasePool.d.ts +48 -0
- package/dist/services/browserLeasePool.d.ts.map +1 -0
- package/dist/services/browserLeasePool.js +210 -0
- package/dist/services/browserLeasePool.js.map +1 -0
- package/dist/services/browserManager.d.ts +6 -7
- package/dist/services/browserManager.d.ts.map +1 -1
- package/dist/services/browserManager.js +122 -177
- package/dist/services/browserManager.js.map +1 -1
- package/dist/services/captureFailure.d.ts +27 -0
- package/dist/services/captureFailure.d.ts.map +1 -0
- package/dist/services/captureFailure.js +134 -0
- package/dist/services/captureFailure.js.map +1 -0
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +45 -134
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/frameCapture.d.ts +14 -3
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +100 -103
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/parallelCoordinator.d.ts +2 -0
- package/dist/services/parallelCoordinator.d.ts.map +1 -1
- package/dist/services/parallelCoordinator.js +37 -6
- package/dist/services/parallelCoordinator.js.map +1 -1
- package/dist/services/screenshotService.d.ts +16 -0
- package/dist/services/screenshotService.d.ts.map +1 -1
- package/dist/services/screenshotService.js +20 -0
- package/dist/services/screenshotService.js.map +1 -1
- package/dist/services/streamingEncoder.d.ts.map +1 -1
- package/dist/services/streamingEncoder.js +23 -58
- package/dist/services/streamingEncoder.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +39 -71
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/utils/ffmpegBinaries.d.ts +2 -2
- package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
- package/dist/utils/ffmpegBinaries.js +8 -82
- package/dist/utils/ffmpegBinaries.js.map +1 -1
- package/dist/utils/ffprobe.d.ts +3 -1
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +35 -42
- package/dist/utils/ffprobe.js.map +1 -1
- package/dist/utils/gpuEncoder.d.ts.map +1 -1
- package/dist/utils/gpuEncoder.js +32 -52
- package/dist/utils/gpuEncoder.js.map +1 -1
- package/dist/utils/managedChildProcess.d.ts +50 -0
- package/dist/utils/managedChildProcess.d.ts.map +1 -0
- package/dist/utils/managedChildProcess.js +148 -0
- package/dist/utils/managedChildProcess.js.map +1 -0
- package/dist/utils/processTracker.js +1 -1
- package/dist/utils/runFfmpeg.d.ts +3 -0
- package/dist/utils/runFfmpeg.d.ts.map +1 -1
- package/dist/utils/runFfmpeg.js +16 -50
- package/dist/utils/runFfmpeg.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
1280
|
-
url
|
|
1281
|
-
failureText
|
|
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
|
-
|
|
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
|
-
|
|
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
|