@hyperframes/engine 0.8.156 → 0.8.158

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.
@@ -4,6 +4,7 @@ export type BrowserPoolState = "launching" | "ready" | "closing";
4
4
  export interface BrowserLaunchFingerprint {
5
5
  readonly args: readonly string[];
6
6
  readonly executablePath?: string;
7
+ readonly withoutWaylandDisplay?: boolean;
7
8
  readonly browserTimeoutMs: number;
8
9
  readonly protocolTimeoutMs: number;
9
10
  readonly requestedCaptureMode: CaptureMode;
@@ -8,6 +8,7 @@ function fingerprintKey(fingerprint) {
8
8
  return JSON.stringify([
9
9
  fingerprint.args,
10
10
  fingerprint.executablePath ?? null,
11
+ fingerprint.withoutWaylandDisplay === true,
11
12
  fingerprint.browserTimeoutMs,
12
13
  fingerprint.protocolTimeoutMs,
13
14
  fingerprint.requestedCaptureMode,
@@ -25,8 +25,6 @@ interface GpuProbeOptions {
25
25
  platform?: NodeJS.Platform;
26
26
  probeLifecycle?: GpuProbeLifecycle;
27
27
  wslHost?: WslHostFacts;
28
- /** Finds full Chrome for the WSL fallback; tests pass a fake. */
29
- findFullChrome?: () => string | undefined;
30
28
  }
31
29
  export type AcquiredBrowser = BrowserLease;
32
30
  /**
@@ -59,11 +57,8 @@ export declare const _probeBeginFrameSupportForTests: typeof probeBeginFrameSupp
59
57
  export declare const _closeBrowserAfterFailedProbeForTests: typeof closeBrowserAfterFailedProbe;
60
58
  /** Test-only: reset the cached probe result. */
61
59
  export declare function _resetAutoBrowserGpuModeCacheForTests(): void;
62
- /**
63
- * The browser a render launches for `resolvedMode`: under WSL, the full Chrome
64
- * the GPU probe fell back to; otherwise the headless shell.
65
- */
66
- export declare function resolveCaptureBrowserPath(resolvedMode: "software" | "hardware", config?: Partial<Pick<EngineConfig, "chromePath">>): Promise<string | undefined>;
60
+ /** True when a `resolvedMode` render must launch without WAYLAND_DISPLAY to reach the GPU. */
61
+ export declare function captureWithoutWaylandDisplay(resolvedMode: "software" | "hardware"): Promise<boolean>;
67
62
  /**
68
63
  * Resolve `browserGpuMode` to a concrete `"software" | "hardware"` answer.
69
64
  *
@@ -91,10 +86,13 @@ export declare function resolveCaptureBrowserPath(resolvedMode: "software" | "ha
91
86
  * render.
92
87
  */
93
88
  export declare function resolveBrowserGpuMode(mode: EngineConfig["browserGpuMode"], options?: GpuProbeOptions): Promise<"software" | "hardware">;
94
- declare function createBrowserLaunchFingerprint(chromeArgs: string[], config?: Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "chromePath" | "forceScreenshot">>): BrowserLaunchFingerprint;
89
+ type BrowserLaunchConfig = Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "chromePath" | "forceScreenshot">> & {
90
+ withoutWaylandDisplay?: boolean;
91
+ };
92
+ declare function createBrowserLaunchFingerprint(chromeArgs: string[], config?: BrowserLaunchConfig): BrowserLaunchFingerprint;
95
93
  /** Test-only export for launch-argument/capture-mode agreement. */
96
94
  export declare const _createBrowserLaunchFingerprintForTests: typeof createBrowserLaunchFingerprint;
97
- export declare function acquireBrowser(chromeArgs: string[], config?: Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "enableBrowserPool" | "chromePath" | "forceScreenshot">>): Promise<AcquiredBrowser>;
95
+ export declare function acquireBrowser(chromeArgs: string[], config?: BrowserLaunchConfig & Partial<Pick<EngineConfig, "enableBrowserPool">>): Promise<AcquiredBrowser>;
98
96
  export declare function releaseBrowser(browser: Browser, _config?: Partial<Pick<EngineConfig, "enableBrowserPool">>): Promise<void>;
99
97
  export declare function forceReleaseBrowser(browser: Browser): void;
100
98
  /**
@@ -13,7 +13,7 @@ import { homedir } from "os";
13
13
  import { chromeMajorCeiling, exceedsChromeCeiling } from "./chromeHostCeiling.js";
14
14
  import { DEFAULT_CONFIG } from "../config.js";
15
15
  import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js";
16
- import { describeWslGpuAccess, detectWsl, } from "./wslHost.js";
16
+ import { describeWslGpuAccess, detectWsl } from "./wslHost.js";
17
17
  import { BrowserLeasePool, } from "./browserLeasePool.js";
18
18
  export { BrowserLeasePool } from "./browserLeasePool.js";
19
19
  let _puppeteer;
@@ -58,6 +58,7 @@ async function probeHardwareWebGlInfo(ppt, options) {
58
58
  args: options.args,
59
59
  defaultViewport: { width: 64, height: 64 },
60
60
  executablePath: options.executablePath,
61
+ env: options.env,
61
62
  timeout: options.browserTimeout,
62
63
  waitForInitialPage: false,
63
64
  };
@@ -427,39 +428,31 @@ function formatProbeFailure(err) {
427
428
  return `probe failed (${err instanceof Error ? err.message : String(err)})`;
428
429
  }
429
430
  async function probeAutoBrowserGpuMode(options) {
430
- const executablePath = options.chromePath ?? resolveHeadlessShellPath({});
431
431
  const ppt = await getPuppeteerOrNull();
432
432
  if (ppt === null) {
433
433
  logResolvedBrowserGpuMode("software", "puppeteer unavailable");
434
434
  return { mode: "software", cause: "probe-error" };
435
435
  }
436
- const probed = await probeWebGlMode(ppt, executablePath, options);
437
- if (probed.mode === "hardware" || !detectWsl(options.wslHost))
436
+ const probed = await probeWebGlMode(ppt, options);
437
+ if (probed.mode === "hardware")
438
438
  return probed;
439
- return probeWslFullChrome(ppt, executablePath, options, probed);
440
- }
441
- // heygen-com/hyperframes#5548: under WSL, chrome-headless-shell can fail EGL
442
- // setup on Mesa's D3D12 driver while full Chrome (--headless=new) gets the GPU.
443
- async function probeWslFullChrome(ppt, shellPath, options, shellOutcome) {
444
- const fullChrome = (options.findFullChrome ?? resolveFullChromePath)();
445
- let route = { state: "missing" };
446
- if (fullChrome) {
447
- const viaFullChrome = fullChrome === shellPath
448
- ? shellOutcome
449
- : await probeWebGlMode(ppt, fullChrome, options, "full Chrome: ");
450
- if (viaFullChrome.mode === "hardware")
451
- return { mode: "hardware", browserPath: fullChrome };
452
- route = { state: viaFullChrome.cause ?? "no-gpu", path: fullChrome };
453
- }
454
- return { ...shellOutcome, wslAccess: describeWslGpuAccess(options.wslHost, route) ?? undefined };
455
- }
456
- async function probeWebGlMode(ppt, executablePath, options, label = "") {
439
+ // Chrome 150+ uses WSLg Wayland EGL, which lacks offscreen configs: https://issues.chromium.org/issues/567222741
440
+ if (detectWsl(options.wslHost) && process.env.WAYLAND_DISPLAY !== undefined) {
441
+ const env = envWithoutWaylandDisplay();
442
+ const retried = await probeWebGlMode(ppt, options, env, "without WAYLAND_DISPLAY: ");
443
+ if (retried.mode === "hardware")
444
+ return { mode: "hardware", withoutWaylandDisplay: true };
445
+ }
446
+ return { ...probed, ...wslAccess(options) };
447
+ }
448
+ async function probeWebGlMode(ppt, options, env, label = "") {
457
449
  try {
458
450
  const info = await probeHardwareWebGlInfo(ppt, {
459
451
  args: getHardwareGpuProbeArgs(options.platform ?? process.platform),
460
452
  browserTimeout: options.browserTimeout ?? DEFAULT_CONFIG.browserTimeout,
461
- executablePath,
453
+ executablePath: options.chromePath ?? resolveHeadlessShellPath({}),
462
454
  probeLifecycle: options.probeLifecycle,
455
+ env,
463
456
  });
464
457
  const resolved = resolveWebGlProbeMode(info);
465
458
  logResolvedBrowserGpuMode(resolved, label + describeWebGlProbe(info));
@@ -470,13 +463,19 @@ async function probeWebGlMode(ppt, executablePath, options, label = "") {
470
463
  return { mode: "software", cause: "probe-error" };
471
464
  }
472
465
  }
473
- /**
474
- * The browser a render launches for `resolvedMode`: under WSL, the full Chrome
475
- * the GPU probe fell back to; otherwise the headless shell.
476
- */
477
- export async function resolveCaptureBrowserPath(resolvedMode, config) {
478
- const probed = resolvedMode === "hardware" ? await _autoBrowserGpuModeCache : undefined;
479
- return probed?.browserPath ?? resolveHeadlessShellPath(config);
466
+ function envWithoutWaylandDisplay() {
467
+ const { WAYLAND_DISPLAY: _wslg, ...env } = process.env;
468
+ return env;
469
+ }
470
+ /** True when a `resolvedMode` render must launch without WAYLAND_DISPLAY to reach the GPU. */
471
+ export async function captureWithoutWaylandDisplay(resolvedMode) {
472
+ if (resolvedMode !== "hardware")
473
+ return false;
474
+ return (await _autoBrowserGpuModeCache)?.withoutWaylandDisplay === true;
475
+ }
476
+ function wslAccess(options) {
477
+ const lines = describeWslGpuAccess(options.wslHost);
478
+ return lines ? { wslAccess: lines } : {};
480
479
  }
481
480
  /**
482
481
  * Resolve `browserGpuMode` to a concrete `"software" | "hardware"` answer.
@@ -620,6 +619,7 @@ function createBrowserLaunchFingerprint(chromeArgs, config) {
620
619
  return {
621
620
  args: chromeArgs,
622
621
  executablePath: headlessShell,
622
+ withoutWaylandDisplay: launchConfig.withoutWaylandDisplay === true,
623
623
  browserTimeoutMs: launchConfig.browserTimeout,
624
624
  protocolTimeoutMs: launchConfig.protocolTimeout,
625
625
  requestedCaptureMode,
@@ -634,6 +634,7 @@ export async function acquireBrowser(chromeArgs, config) {
634
634
  // fallow-ignore-next-line complexity
635
635
  async function launchBrowser(fingerprint) {
636
636
  const ppt = await getPuppeteer();
637
+ const env = fingerprint.withoutWaylandDisplay ? envWithoutWaylandDisplay() : undefined;
637
638
  let captureMode = fingerprint.requestedCaptureMode;
638
639
  let browser;
639
640
  try {
@@ -643,6 +644,7 @@ async function launchBrowser(fingerprint) {
643
644
  args: [...fingerprint.args],
644
645
  defaultViewport: null,
645
646
  executablePath: fingerprint.executablePath,
647
+ env,
646
648
  timeout: fingerprint.browserTimeoutMs,
647
649
  protocolTimeout: fingerprint.protocolTimeoutMs,
648
650
  });
@@ -667,6 +669,7 @@ async function launchBrowser(fingerprint) {
667
669
  args: stripBeginFrameFlags([...fingerprint.args]),
668
670
  defaultViewport: null,
669
671
  executablePath: fingerprint.executablePath,
672
+ env,
670
673
  timeout: fingerprint.browserTimeoutMs,
671
674
  protocolTimeout: fingerprint.protocolTimeoutMs,
672
675
  });
@@ -15,7 +15,7 @@ import { DrawElementCaptureError } from "./drawElementCaptureError.js";
15
15
  import { encodePng } from "../utils/alphaBlit.js";
16
16
  import { DEFAULT_SAMPLES_PER_FRAME, MotionBlurAccumulator, SPATIAL_TWEEN_PROPERTIES, adaptiveSampleCount, motionBlurProbeTimes, motionBlurSampleTimes, motionBlurWindowIsStatic, probeDiffMagnitude, resolveMotionBlurPlan, } from "./motionBlur.js";
17
17
  // ── Extracted modules ───────────────────────────────────────────────────────
18
- import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, resolveCaptureBrowserPath, resolveHeadlessShellPath, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, } from "./browserManager.js";
18
+ import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, captureWithoutWaylandDisplay, resolveHeadlessShellPath, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, } from "./browserManager.js";
19
19
  import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
20
20
  import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, DE_CANVAS_NOT_INITIALIZED_CODE, } from "./drawElementService.js";
21
21
  import { initThreeDProjection, detectCssEffectRisk } from "./threeDProjection.js";
@@ -786,10 +786,7 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
786
786
  // boolean `forceScreenshot === false` is otherwise ambiguous between
787
787
  // default and explicit opt-out).
788
788
  const effectiveForceScreenshot = applyConcreteGpuScreenshotClamp(forceScreenshot, resolvedGpuMode, config);
789
- const browserPath = await resolveCaptureBrowserPath(resolvedGpuMode, config);
790
- const wslFullChromeHasNoBeginFrame = browserPath !== headlessShell;
791
789
  const preMode = headlessShell &&
792
- !wslFullChromeHasNoBeginFrame &&
793
790
  isLinux &&
794
791
  !effectiveForceScreenshot &&
795
792
  !supersampling &&
@@ -807,7 +804,8 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
807
804
  .catch(() => false));
808
805
  const gpuConfig = { ...config, browserGpuMode: resolvedGpuMode };
809
806
  const chromeArgs = buildChromeArgs({ width: options.width, height: options.height, captureMode: preMode, requiresWebGpu }, gpuConfig);
810
- const browserLease = await acquireBrowser(chromeArgs, { ...config, chromePath: browserPath });
807
+ const withoutWaylandDisplay = await captureWithoutWaylandDisplay(resolvedGpuMode);
808
+ const browserLease = await acquireBrowser(chromeArgs, { ...config, withoutWaylandDisplay });
811
809
  return constructCaptureSessionWithRollback({
812
810
  browserLease,
813
811
  serverUrl,
@@ -17,14 +17,8 @@ export interface WslHostFacts {
17
17
  renderNodeAccess(path: string): RenderNodeAccess;
18
18
  }
19
19
  export declare function detectWsl(host?: WslHostFacts): boolean;
20
- export type FullChromeRoute = {
21
- state: "missing";
22
- } | {
23
- state: "no-gpu" | "probe-error";
24
- path: string;
25
- };
26
20
  /**
27
21
  * Indented lines saying what we checked on the WSL GPU path and what to try,
28
22
  * or null outside WSL. Says nothing about the render mode: callers own that.
29
23
  */
30
- export declare function describeWslGpuAccess(host?: WslHostFacts, fullChrome?: FullChromeRoute): string[] | null;
24
+ export declare function describeWslGpuAccess(host?: WslHostFacts): string[] | null;
@@ -57,24 +57,11 @@ export function detectWsl(host = realHost) {
57
57
  const RENDER_NODE = "/dev/dri/renderD128";
58
58
  const NATIVE_WINDOWS = "render from native Windows (project copied to a Windows drive, run from PowerShell), " +
59
59
  "where Chrome uses the GPU through Direct3D.";
60
- function routeToGpu(fullChrome) {
61
- const viaFullChrome = fullChrome.state === "missing"
62
- ? [
63
- " Under WSL, Chrome's headless shell often cannot reach the GPU while full Chrome can. " +
64
- "Install full Chrome with `npx @puppeteer/browsers install chrome@stable --path " +
65
- "~/.cache/puppeteer` and HyperFrames will try it when the headless shell finds no GPU " +
66
- "(or point HYPERFRAMES_BROWSER_PATH at a full Chrome).",
67
- ]
68
- : fullChrome.state === "no-gpu"
69
- ? [` Full Chrome (${fullChrome.path}) found no hardware GPU either.`]
70
- : [` Full Chrome (${fullChrome.path}) could not finish its GPU check either.`];
71
- return [...viaFullChrome, ` Or ${NATIVE_WINDOWS}`];
72
- }
73
60
  /**
74
61
  * Indented lines saying what we checked on the WSL GPU path and what to try,
75
62
  * or null outside WSL. Says nothing about the render mode: callers own that.
76
63
  */
77
- export function describeWslGpuAccess(host = realHost, fullChrome = { state: "missing" }) {
64
+ export function describeWslGpuAccess(host = realHost) {
78
65
  if (!detectWsl(host))
79
66
  return null;
80
67
  const access = host.renderNodeAccess(RENDER_NODE);
@@ -82,21 +69,21 @@ export function describeWslGpuAccess(host = realHost, fullChrome = { state: "mis
82
69
  return [
83
70
  ` Your Linux user cannot read and write ${RENDER_NODE} and is not in its group ` +
84
71
  `("${access.group}"), which can stop Mesa from reaching the GPU.`,
85
- ` First run \`sudo usermod -aG ${access.group} $USER\`, then \`wsl --shutdown\` from ` +
86
- "Windows and reopen WSL. On its own that may not be enough:",
87
- ...routeToGpu(fullChrome),
72
+ ` Fix: run \`sudo usermod -aG ${access.group} $USER\`, then \`wsl --shutdown\` from ` +
73
+ "Windows, reopen WSL and render again.",
74
+ ` Or ${NATIVE_WINDOWS}`,
88
75
  ];
89
76
  }
90
77
  if (access.state === "denied") {
91
78
  return [
92
79
  ` Your Linux user is in the "${access.group}" group but still cannot read and write ` +
93
80
  `${RENDER_NODE}; check its permissions with \`ls -l ${RENDER_NODE}\`.`,
94
- ...routeToGpu(fullChrome),
81
+ ` Or ${NATIVE_WINDOWS}`,
95
82
  ];
96
83
  }
97
84
  const seen = access.state === "missing" ? "does not exist" : "is readable and writable";
98
85
  return [
99
86
  ` ${RENDER_NODE} ${seen}, so file permissions are not the cause.`,
100
- ...routeToGpu(fullChrome),
87
+ ` To use the GPU, ${NATIVE_WINDOWS}`,
101
88
  ];
102
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.156",
3
+ "version": "0.8.158",
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.156",
54
- "@hyperframes/core": "^0.8.156"
53
+ "@hyperframes/parsers": "^0.8.158",
54
+ "@hyperframes/core": "^0.8.158"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",