fix(engine): auto-fall back to screenshot mode when chrome-headless-shell drops HeadlessExperimental.beginFrame - #296
Conversation
Chromium 132+ removed the HeadlessExperimental domain. On Linux with chrome-headless-shell we default to beginframe capture, which silently produces blank frames on newer Chromium because `--enable-begin-frame-control` leaves the compositor waiting for beginFrames the code can no longer send. Users had to discover PRODUCER_FORCE_SCREENSHOT=true themselves. Probe HeadlessExperimental.enable() on a disposable page right after launch. If the domain isn't there, close the browser, relaunch with the beginframe- only chrome flags stripped, and mark the session as screenshot mode. Users on supported Chromium versions are unaffected — the probe adds one CDP round-trip to browser acquisition and nothing else. Closes #294.
|
Fact-check on the description's version attribution — the code looks right, but the "Chromium 132 removed the entire What Chromium 132 actually did: removed the old What's actually in tip-of-tree Chromium today (
Issue #294 itself pins the failure on Chromium 147, which matches the branch name. Whatever dropped Practical risk for the fix: the probe calls Suggested tweak — rewrite the first paragraph roughly as:
Code itself LGTM — just the version attribution. |
First version of the probe only called `HeadlessExperimental.enable`, but some Chromium builds keep the domain registered (so enable() succeeds) and drop the beginFrame method itself. The warmup loop then crashes on first frame with `'HeadlessExperimental.beginFrame' wasn't found` — exactly the failure mode reported in #294. Race one cheap beginFrame call against a 2s timeout. Any failure — method missing, timeout, generic protocol error — is treated as unsupported and the caller relaunches in screenshot mode. Verified against chrome-headless- shell 146 on Linux x86_64 (real beginFrame support, probe returns true) and forced-fail mode (probe returns false → strip beginframe flags → relaunch → valid 6.8 KB PNG captured). Found via devbox testing #294 repro on Linux.
Chromium 132 removed the old `--headless` mode from the main Chrome binary, not the HeadlessExperimental CDP domain. The domain is still upstream (enable/disable are deprecated but beginFrame is not) — what actually dropped in user reports is chrome-headless-shell 147+ builds, which is where issue #294 reproduces. Rewrites the doc comment and the inline comment in acquireBrowser to attribute the breakage to chrome-headless-shell 147 rather than all of Chromium 132+. No behavioural change. Per review feedback on #296.
|
Thanks @vanceingalls — both points addressed: 1. Version attribution. Fixed in 7c0d1ae. Updated the PR title + description and the inline doc comments to pin the breakage on 2. Probe
This catches exactly the build shape you're worried about — Let me know if the current probe's failure handling is too broad — I generalised to |
Merge activity
|
Chromium 132 removed the old `--headless` mode from the main Chrome binary, not the HeadlessExperimental CDP domain. The domain is still upstream (enable/disable are deprecated but beginFrame is not) — what actually dropped in user reports is chrome-headless-shell 147+ builds, which is where issue heygen-com#294 reproduces. Rewrites the doc comment and the inline comment in acquireBrowser to attribute the breakage to chrome-headless-shell 147 rather than all of Chromium 132+. No behavioural change. Per review feedback on heygen-com#296.
…hell drops HeadlessExperimental.beginFrame (heygen-com#296) Closes heygen-com#294. ## Summary Recent `chrome-headless-shell` builds (observed on 147) no longer expose `HeadlessExperimental.beginFrame`. The domain's `enable`/`disable` methods are deprecated upstream and appear to have been dropped alongside `beginFrame` in these builds, so on Linux with chrome-headless-shell the engine aborts with \`\`\` Protocol error (HeadlessExperimental.beginFrame): 'HeadlessExperimental.beginFrame' wasn't found \`\`\` and — because the browser was launched with `--enable-begin-frame-control` — the compositor waits for beginFrames the engine can no longer deliver, so every subsequent screenshot also comes back blank. Today users have to discover `PRODUCER_FORCE_SCREENSHOT=true` themselves (openclaw did exactly that — see the issue body). ## Fix One-time probe, right after the browser launches in beginframe mode: 1. Create a disposable CDP session. 2. `await client.send("HeadlessExperimental.enable")`. 3. Send one no-op `HeadlessExperimental.beginFrame` raced against a 2s timeout. 4. If anything throws / times out — missing method, protocol error, stuck call — close the browser, strip beginframe-only chrome flags, relaunch in screenshot mode, and set \`captureMode = "screenshot"\` for the returned session. Probing `beginFrame` directly rather than `enable` alone is important because some builds keep the domain registered (so `.enable()` succeeds) while dropping the method itself — that's exactly the failure shape in heygen-com#294. Cost on happy path: one extra CDP round-trip per browser acquisition (≈ a few ms, since in beginframe-control mode the command returns as soon as the compositor acks). Cost on broken path: one extra launch, which is what the env-var escape hatch already forces manually. The beginframe-only flag set is enumerated in-module and matched by the stripper, so adding/removing flags stays in one place with `buildChromeArgs`. ## Test plan - [x] `bun run --filter=@hyperframes/engine test` — all 42 tests pass - [x] `bun run --filter=@hyperframes/engine build` — typechecks - [x] `bunx oxlint` + `bunx oxfmt --check` clean - [x] Manual: standalone test on Linux x86_64 with chrome-headless-shell 146 — probe returns `supported=true`, no fallback (happy path) - [x] Manual: same test with `--force-fail` simulating openclaw's missing-method condition — fallback triggers, flags stripped, relaunch succeeds, 6.8 KB PNG captured (broken path) - [ ] Verify on openclaw / real chrome-headless-shell 147 build that the fallback triggers automatically without `PRODUCER_FORCE_SCREENSHOT` ## Notes - `probeBeginFrameSupport` catches any failure generically; we trust that a working browser answers the no-op beginFrame in well under 2s. - Warning is logged once per browser acquisition, not per frame. - Browser pool interaction: pooled browsers cache the resolved `captureMode`, so subsequent acquires in the same process reuse the post-fallback mode without re-probing.
Closes #294.
Summary
Recent
chrome-headless-shellbuilds (observed on 147) no longer exposeHeadlessExperimental.beginFrame. The domain'senable/disablemethods are deprecated upstream and appear to have been dropped alongsidebeginFramein these builds, so on Linux with chrome-headless-shell the engine aborts with```
Protocol error (HeadlessExperimental.beginFrame):
'HeadlessExperimental.beginFrame' wasn't found
```
and — because the browser was launched with
--enable-begin-frame-control— the compositor waits for beginFrames the engine can no longer deliver, so every subsequent screenshot also comes back blank. Today users have to discoverPRODUCER_FORCE_SCREENSHOT=truethemselves (openclaw did exactly that — see the issue body).Fix
One-time probe, right after the browser launches in beginframe mode:
await client.send("HeadlessExperimental.enable").HeadlessExperimental.beginFrameraced against a 2s timeout.Probing
beginFramedirectly rather thanenablealone is important because some builds keep the domain registered (so.enable()succeeds) while dropping the method itself — that's exactly the failure shape in #294.Cost on happy path: one extra CDP round-trip per browser acquisition (≈ a few ms, since in beginframe-control mode the command returns as soon as the compositor acks). Cost on broken path: one extra launch, which is what the env-var escape hatch already forces manually.
The beginframe-only flag set is enumerated in-module and matched by the stripper, so adding/removing flags stays in one place with
buildChromeArgs.Test plan
bun run --filter=@hyperframes/engine test— all 42 tests passbun run --filter=@hyperframes/engine build— typechecksbunx oxlint+bunx oxfmt --checkcleansupported=true, no fallback (happy path)--force-failsimulating openclaw's missing-method condition — fallback triggers, flags stripped, relaunch succeeds, 6.8 KB PNG captured (broken path)PRODUCER_FORCE_SCREENSHOTNotes
probeBeginFrameSupportcatches any failure generically; we trust that a working browser answers the no-op beginFrame in well under 2s.captureMode, so subsequent acquires in the same process reuse the post-fallback mode without re-probing.