[GH-ISSUE #301] bug: Personal Chrome connects and shares, but encoder never emits a keyframe — connect_personal times out after 90s with an empty error #214

Open
opened 2026-10-18 14:03:51 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @PiefkePaul on GitHub (Sep 22, 2026).
Original GitHub issue: https://github.com/open-webui/computer/issues/301

Summary

Connecting "Browser Tools" → "Personal Chrome" (real, already-running Chrome via chrome://inspect/#remote-debugging, not the managed/headless browser) completes every step up through the OS-level window-share permission — Chrome shows the capture-sharing indicator bar — but the encoder in browser-encoder.html never produces a first video keyframe. The request blocks for exactly 90 seconds and then fails with HTTP 409 {"detail":""}, and the frontend shows an indefinite loading spinner with no visible error at all.

Environment

  • cptr version: 0.9.21
  • OS: Windows 11
  • Chrome: reproduced on both 153.0.8010.50 and 153.0.8010.53 (updated mid-investigation — no change in behavior)
  • browser.provider: local, browser.tab_default_mode: chrome, browser.tab_chrome_source: personal

Steps to reproduce

  1. In Chrome, open chrome://inspect/#remote-debugging and enable remote debugging on the already-running (personal) Chrome instance. Note the live port it shows (e.g. 127.0.0.1:63918).
  2. In cptr Settings → Websuche → Browsersteuerung → Browser-Tools für Agenten: enable, provider "Lokales CDP", or independently under "Browser-Tabs" → "Standard für Browser-Tabs": Chrome, "Chrome-Profil": Persönliches Chrome, CDP-URL = the port from step 1.
  3. Confirm the raw connection works: POST /api/browser/cdp {"url": "http://127.0.0.1:<port>"} → 200 {"browser":"Chrome/..."}. This always succeeds.
  4. Click "Verbinden" (Settings) or open a new "Browser" tab (+ → Browser), which calls POST /api/browser/personal.
  5. In Chrome, the "Choose what to share" dialog appears (window-list picker). Select the window titled "Open WebUI Computer Browser". Approve.
  6. Chrome shows the "is sharing a window" indicator bar — capture is genuinely active on the Chrome side.
  7. Wait. After 90 seconds: POST /api/browser/personal → 409 {"detail":""}. The tab UI never updates past "Browser wird gestartet…" (no error is ever surfaced to the user in the UI itself — only visible via the browser console: Failed to create browser session: o: Conflict).

Server-side evidence

09:52:16.799 INFO  WebSocket /api/browser/sessions/personal/encoder?token=... [accepted]
09:52:16.800 INFO  connection open
09:53:46.794 INFO  POST /api/browser/personal HTTP/1.1 409

Exactly 90.0s between encoder accept and the 409 — matches asyncio.wait_for(personal.first_keyframe.wait(), 90) in viewer.py's connect_personal. asyncio.TimeoutError stringifies to "", which is why the raised HTTPException(status_code=409, detail=str(exc)) in routers/browser.py carries no useful message — reproducible root cause of the empty {"detail":""}, and worth fixing as a UX issue on its own (the frontend should at least show something instead of an infinite spinner on any 4xx/5xx from this endpoint).

The encoder WebSocket connects and stays open the whole 90s (confirmed both via server log and via the Chrome wird von automatisierter Testsoftware gesteuert banner + active share indicator visible in the shared window) — so the failure is specifically that no type: "config"/frame data is ever received from the browser-encoder.html page's own capture pipeline (window.startCapture()), not a connection or permission problem.

What I tried, no effect

  • Setting browser.encoder.hardware_acceleration to prefer-software instead of no-preference — same 90s timeout.
  • Updating Chrome (153.0.8010.50 → .53) mid-investigation — identical behavior before and after.
  • Restarting cptr fully between attempts (rules out stale in-process state as the sole cause — first attempt after a clean restart also times out the same way).

browser.cdp_url is one config key used by both "Browser-Tools für Agenten" (agent tool — needs headless Chrome, no real window) and "Browser-Tabs → Chrome → Personal" (needs a real, capturable window). Pointing it at a headless agent Chrome breaks Personal mode outright (no window to capture), and pointing it at the personal Chrome breaks the agent tool. There's no way to have both configured at once in the current UI/schema. Might be worth a second, independent config key for the two use cases, since they're fundamentally different capture mechanisms.

Suggested next step

Since the capture pipeline gets all the way to an active, user-approved share with a live encoder WebSocket but never emits a keyframe, I'd guess the issue is in the browser-encoder.html/browser-runtime.js WebCodecs setup (or the getDisplayMedia/chromeMediaSource stream handoff) rather than anything CDP- or permission-related. Happy to provide more logs/traces if useful — I have a full reproducible timeline including screenshots of the Chrome share picker and the (blank) encoder page mid-capture.

Originally created by @PiefkePaul on GitHub (Sep 22, 2026). Original GitHub issue: https://github.com/open-webui/computer/issues/301 ## Summary Connecting "Browser Tools" → "Personal Chrome" (real, already-running Chrome via `chrome://inspect/#remote-debugging`, not the managed/headless browser) completes every step up through the OS-level window-share permission — Chrome shows the capture-sharing indicator bar — but the encoder in `browser-encoder.html` never produces a first video keyframe. The request blocks for exactly 90 seconds and then fails with `HTTP 409 {"detail":""}`, and the frontend shows an indefinite loading spinner with no visible error at all. ## Environment - cptr version: 0.9.21 - OS: Windows 11 - Chrome: reproduced on both 153.0.8010.50 and 153.0.8010.53 (updated mid-investigation — no change in behavior) - `browser.provider`: `local`, `browser.tab_default_mode`: `chrome`, `browser.tab_chrome_source`: `personal` ## Steps to reproduce 1. In Chrome, open `chrome://inspect/#remote-debugging` and enable remote debugging on the already-running (personal) Chrome instance. Note the live port it shows (e.g. `127.0.0.1:63918`). 2. In cptr Settings → Websuche → Browsersteuerung → Browser-Tools für Agenten: enable, provider "Lokales CDP", **or** independently under "Browser-Tabs" → "Standard für Browser-Tabs": `Chrome`, "Chrome-Profil": `Persönliches Chrome`, CDP-URL = the port from step 1. 3. Confirm the raw connection works: `POST /api/browser/cdp {"url": "http://127.0.0.1:<port>"}` → `200 {"browser":"Chrome/..."}`. This always succeeds. 4. Click "Verbinden" (Settings) or open a new "Browser" tab (`+` → Browser), which calls `POST /api/browser/personal`. 5. In Chrome, the "Choose what to share" dialog appears (window-list picker). Select the window titled "Open WebUI Computer Browser". Approve. 6. Chrome shows the "is sharing a window" indicator bar — capture is genuinely active on the Chrome side. 7. Wait. After 90 seconds: `POST /api/browser/personal` → `409 {"detail":""}`. The tab UI never updates past "Browser wird gestartet…" (no error is ever surfaced to the user in the UI itself — only visible via the browser console: `Failed to create browser session: o: Conflict`). ## Server-side evidence ``` 09:52:16.799 INFO WebSocket /api/browser/sessions/personal/encoder?token=... [accepted] 09:52:16.800 INFO connection open 09:53:46.794 INFO POST /api/browser/personal HTTP/1.1 409 ``` Exactly 90.0s between encoder accept and the 409 — matches `asyncio.wait_for(personal.first_keyframe.wait(), 90)` in `viewer.py`'s `connect_personal`. `asyncio.TimeoutError` stringifies to `""`, which is why the raised `HTTPException(status_code=409, detail=str(exc))` in `routers/browser.py` carries no useful message — reproducible root cause of the empty `{"detail":""}`, and worth fixing as a UX issue on its own (the frontend should at least show *something* instead of an infinite spinner on any 4xx/5xx from this endpoint). The encoder WebSocket connects and stays open the whole 90s (confirmed both via server log and via the `Chrome wird von automatisierter Testsoftware gesteuert` banner + active share indicator visible in the shared window) — so the failure is specifically that no `type: "config"`/frame data is ever received from the `browser-encoder.html` page's own capture pipeline (`window.startCapture()`), not a connection or permission problem. ## What I tried, no effect - Setting `browser.encoder.hardware_acceleration` to `prefer-software` instead of `no-preference` — same 90s timeout. - Updating Chrome (153.0.8010.50 → .53) mid-investigation — identical behavior before and after. - Restarting cptr fully between attempts (rules out stale in-process state as the sole cause — first attempt after a clean restart also times out the same way). ## Separate but related: shared `cdp_url` field `browser.cdp_url` is one config key used by both "Browser-Tools für Agenten" (agent tool — needs headless Chrome, no real window) and "Browser-Tabs → Chrome → Personal" (needs a real, capturable window). Pointing it at a headless agent Chrome breaks Personal mode outright (no window to capture), and pointing it at the personal Chrome breaks the agent tool. There's no way to have both configured at once in the current UI/schema. Might be worth a second, independent config key for the two use cases, since they're fundamentally different capture mechanisms. ## Suggested next step Since the capture pipeline gets all the way to an active, user-approved share with a live encoder WebSocket but never emits a keyframe, I'd guess the issue is in the `browser-encoder.html`/`browser-runtime.js` WebCodecs setup (or the `getDisplayMedia`/`chromeMediaSource` stream handoff) rather than anything CDP- or permission-related. Happy to provide more logs/traces if useful — I have a full reproducible timeline including screenshots of the Chrome share picker and the (blank) encoder page mid-capture.
Author
Owner

@owui-terminator[bot] commented on GitHub (Sep 22, 2026):

🔍 Related Issues Found

I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions:

  1. 🟣 #156 bug: Browser streaming fails on any host without a hardware video encoder
    This is the closest match on the browser encoding pipeline. It also concerns browser-encoder.js / WebCodecs video encoding failures, so it may share the same area of code even though the specific symptom here is a 90s timeout waiting for the first keyframe.
    by knguyen298

  2. 🟢 #240 bug: Managed Chrome startup failed:
    This is another Chrome startup-related browser connection issue in the same subsystem. It is less specific than the encoder problem, but it may be useful context for related browser-mode failures in connect_personal/Chrome session startup.
    by pixel4696


💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead.

This comment was generated automatically. React with 👍 if helpful, 👎 if not.

<!-- gh-comment-id:5773562507 --> @owui-terminator[bot] commented on GitHub (Sep 22, 2026): <!-- terminator-bot:related-issues-reply --> 🔍 **Related Issues Found** I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions: 1. 🟣 [#156](https://github.com/open-webui/computer/issues/156) **bug: Browser streaming fails on any host without a hardware video encoder** *This is the closest match on the browser encoding pipeline. It also concerns `browser-encoder.js` / WebCodecs video encoding failures, so it may share the same area of code even though the specific symptom here is a 90s timeout waiting for the first keyframe.* *by knguyen298* 2. 🟢 [#240](https://github.com/open-webui/computer/issues/240) **bug: Managed Chrome startup failed:** *This is another Chrome startup-related browser connection issue in the same subsystem. It is less specific than the encoder problem, but it may be useful context for related browser-mode failures in `connect_personal`/Chrome session startup.* *by pixel4696* --- 💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead. *This comment was generated automatically.* React with 👍 if helpful, 👎 if not.
Author
Owner

@PiefkePaul commented on GitHub (Sep 22, 2026):

Checked the two related issues the bot linked:

  • #240 — different subsystem entirely (Linux/Xvfb managed-Chrome startup). Not related.
  • #156 — closer (same file, WebCodecs H.264 config), but already fixed and closed; its fix (hardwareAcceleration: 'prefer-hardware' → 'no-preference') throws an explicit, immediate error when the config is unsupported. My repro was already running with no-preference throughout (confirmed via browser.encoder.hardware_acceleration), and the failure is a silent 90s timeout with no error at all — not what #156 describes.

Traced it further using the installed 0.9.21 source (cptr/utils/browser/viewer.py + cptr/frontend/build/browser-encoder.js) and found what looks like the actual root cause — a deadlock, not an environment issue:

  • browser-encoder.js starts with paused = true (line 16). The encode loop only ever calls configure()/encoder.encode() when !paused (lines 196–200) — otherwise every captured frame is silently read and discarded, no error, no signal.
  • The only place that sends {"type": "resume"} to the personal encoder is _set_visibility() (viewer.py ~line 1592), which fires when a viewer (a peer WebSocket for an actual browser tab watching the stream) reports visible: true.
  • But connect_personal() (viewer.py ~line 680) calls window.startCapture() and then does await asyncio.wait_for(personal.first_keyframe.wait(), 90) before any tab/viewer can possibly exist — attach_personal() only creates the tab session after connect_personal() already returns, and the frontend only opens a viewer socket after that.

So on a fresh connection there is no code path that ever sends resume before the 90s wait starts — the encoder stays paused for the entire wait, first_keyframe can never be set, and the call times out with an empty asyncio.TimeoutError (which is why detail is always ""). This isn't hardware/OS/Chrome-version specific — I reproduced it identically across a Chrome update (153.0.8010.50 → .53) and across full cptr process restarts.

Possible fix directions:

  • Send resume (or skip the pause) for the personal encoder immediately in connect_personal, before waiting on first_keyframe, since there's no viewer yet to gate it on.
  • Or don't wait on first_keyframe in connect_personal at all — let attach_personal/the viewer socket's own _set_visibility call drive the first frame instead, same as it already does for reconnects.

Happy to test a patched browser-encoder.js/viewer.py locally if that's useful — I have a fully reproducible setup (Windows 11, real personal Chrome via chrome://inspect/#remote-debugging, confirmed capture-permission grant, full server-side logs).

<!-- gh-comment-id:5773629600 --> @PiefkePaul commented on GitHub (Sep 22, 2026): Checked the two related issues the bot linked: - **#240** — different subsystem entirely (Linux/Xvfb managed-Chrome startup). Not related. - **#156** — closer (same file, WebCodecs H.264 config), but already fixed and closed; its fix (`hardwareAcceleration: 'prefer-hardware' → 'no-preference'`) throws an *explicit, immediate* error when the config is unsupported. My repro was already running with `no-preference` throughout (confirmed via `browser.encoder.hardware_acceleration`), and the failure is a silent 90s timeout with no error at all — not what #156 describes. Traced it further using the installed 0.9.21 source (`cptr/utils/browser/viewer.py` + `cptr/frontend/build/browser-encoder.js`) and found what looks like the actual root cause — a deadlock, not an environment issue: - `browser-encoder.js` starts with `paused = true` (line 16). The encode loop only ever calls `configure()`/`encoder.encode()` when `!paused` (lines 196–200) — otherwise every captured frame is silently read and discarded, no error, no signal. - The only place that sends `{"type": "resume"}` to the personal encoder is `_set_visibility()` (`viewer.py` ~line 1592), which fires when a **viewer** (a peer WebSocket for an actual browser tab watching the stream) reports `visible: true`. - But `connect_personal()` (`viewer.py` ~line 680) calls `window.startCapture()` and then does `await asyncio.wait_for(personal.first_keyframe.wait(), 90)` **before any tab/viewer can possibly exist** — `attach_personal()` only creates the tab session *after* `connect_personal()` already returns, and the frontend only opens a viewer socket after that. So on a fresh connection there is no code path that ever sends `resume` before the 90s wait starts — the encoder stays paused for the entire wait, `first_keyframe` can never be set, and the call times out with an empty `asyncio.TimeoutError` (which is why `detail` is always `""`). This isn't hardware/OS/Chrome-version specific — I reproduced it identically across a Chrome update (153.0.8010.50 → .53) and across full `cptr` process restarts. Possible fix directions: - Send `resume` (or skip the pause) for the personal encoder immediately in `connect_personal`, before waiting on `first_keyframe`, since there's no viewer yet to gate it on. - Or don't wait on `first_keyframe` in `connect_personal` at all — let `attach_personal`/the viewer socket's own `_set_visibility` call drive the first frame instead, same as it already does for reconnects. Happy to test a patched `browser-encoder.js`/`viewer.py` locally if that's useful — I have a fully reproducible setup (Windows 11, real personal Chrome via `chrome://inspect/#remote-debugging`, confirmed capture-permission grant, full server-side logs).
Author
Owner

@PiefkePaul commented on GitHub (Sep 22, 2026):

Tested the fix locally against the exact hypothesis above — confirmed, end to end, with a live personal Chrome (real profile, remote debugging enabled via chrome://inspect/#remote-debugging, capture permission approved each time).

Patch 1 — the deadlock (root cause)

viewer.py, _personal_encoder_socket, right after personal.encoder = websocket:

await websocket.accept()
personal.encoder = websocket
with contextlib.suppress(Exception):
    await websocket.send_json({"type": "resume"})
try:
    ...

Result: the 90s timeout is gone — connect_personal now progresses in a few seconds. But this immediately surfaced a second, previously-unreachable bug:

Patch 2 — crop() x/y not sample-aligned

browser-encoder.js, crop(): width/height are rounded to even (Math.floor(n/2)*2), but x/y are not, so VideoFrame construction throws Invalid visibleRect. x is not sample-aligned in plane 1. (YUV420 needs even chroma-plane offsets) whenever the unrounded offset happens to be odd — which, on a real window, it does almost every time.

// before
return {
  x: Math.max(0, Math.floor((frame.codedWidth - width) / 2)),
  y: Math.max(0, frame.codedHeight - height),
  width,
  height
};

// after
const rawX = Math.max(0, Math.floor((frame.codedWidth - width) / 2));
const rawY = Math.max(0, frame.codedHeight - height);
return {
  x: Math.floor(rawX / 2) * 2,
  y: Math.floor(rawY / 2) * 2,
  width,
  height
};

Bonus gotcha while testing patch 2

browser-encoder.html/.js are served via a bare FileResponse with no Cache-Control header (app.py's catch-all serve_spa). A real (non-headless) Chrome heuristically caches them and does not revisit the network for a long time, so after patching browser-encoder.js the old cached copy kept loading anyway — including across brand-new CDP-created windows, since the URL is unversioned (/browser-encoder.html → <script src="/browser-encoder.js">, both without cache-busting). Had to append a cache-busting query string to the controller URL in connect_personal (f"{base}/browser-encoder.html?patch301#{fragment}") before the fix actually took effect client-side. Worth adding a real Cache-Control: no-store (matching what /api/browser/runtime.js already does) or a build-hash-versioned filename regardless of the two bugs above — otherwise every future fix to this file has the same silent-cache trap during dev/testing, and possibly for end users updating cptr too (stale JS surviving a version bump).

Result after both patches + cache-bust

POST /api/browser/personal -> 200 {"status":"playing","browser":"Chrome","session_count":0}
POST /api/browser/sessions {"mode":"chrome","url":"https://example.com"} -> 200 {"session_id":"...","mode":"chrome","status":"playing"}

Clean, no errors, ~3s total. Happy to open a PR with these two diffs if that's useful — they're both minimal and don't touch any other code paths (crop() early-returns unchanged when !windowCapture, i.e. the managed-Chrome viewer is untouched).

<!-- gh-comment-id:5773882708 --> @PiefkePaul commented on GitHub (Sep 22, 2026): Tested the fix locally against the exact hypothesis above — confirmed, end to end, with a live personal Chrome (real profile, remote debugging enabled via `chrome://inspect/#remote-debugging`, capture permission approved each time). ## Patch 1 — the deadlock (root cause) `viewer.py`, `_personal_encoder_socket`, right after `personal.encoder = websocket`: ```python await websocket.accept() personal.encoder = websocket with contextlib.suppress(Exception): await websocket.send_json({"type": "resume"}) try: ... ``` Result: the 90s timeout is gone — `connect_personal` now progresses in a few seconds. But this immediately surfaced a **second, previously-unreachable bug**: ## Patch 2 — crop() x/y not sample-aligned `browser-encoder.js`, `crop()`: `width`/`height` are rounded to even (`Math.floor(n/2)*2`), but `x`/`y` are not, so `VideoFrame` construction throws `Invalid visibleRect. x is not sample-aligned in plane 1.` (YUV420 needs even chroma-plane offsets) whenever the unrounded offset happens to be odd — which, on a real window, it does almost every time. ```js // before return { x: Math.max(0, Math.floor((frame.codedWidth - width) / 2)), y: Math.max(0, frame.codedHeight - height), width, height }; // after const rawX = Math.max(0, Math.floor((frame.codedWidth - width) / 2)); const rawY = Math.max(0, frame.codedHeight - height); return { x: Math.floor(rawX / 2) * 2, y: Math.floor(rawY / 2) * 2, width, height }; ``` ## Bonus gotcha while testing patch 2 `browser-encoder.html`/`.js` are served via a bare `FileResponse` with no `Cache-Control` header (`app.py`'s catch-all `serve_spa`). A real (non-headless) Chrome heuristically caches them and does **not** revisit the network for a long time, so after patching `browser-encoder.js` the *old* cached copy kept loading anyway — including across brand-new CDP-created windows, since the URL is unversioned (`/browser-encoder.html` → `<script src="/browser-encoder.js">`, both without cache-busting). Had to append a cache-busting query string to the controller URL in `connect_personal` (`f"{base}/browser-encoder.html?patch301#{fragment}"`) before the fix actually took effect client-side. Worth adding a real `Cache-Control: no-store` (matching what `/api/browser/runtime.js` already does) or a build-hash-versioned filename regardless of the two bugs above — otherwise every future fix to this file has the same silent-cache trap during dev/testing, and possibly for end users updating cptr too (stale JS surviving a version bump). ## Result after both patches + cache-bust ``` POST /api/browser/personal -> 200 {"status":"playing","browser":"Chrome","session_count":0} POST /api/browser/sessions {"mode":"chrome","url":"https://example.com"} -> 200 {"session_id":"...","mode":"chrome","status":"playing"} ``` Clean, no errors, ~3s total. Happy to open a PR with these two diffs if that's useful — they're both minimal and don't touch any other code paths (crop() early-returns unchanged when `!windowCapture`, i.e. the managed-Chrome viewer is untouched).
Author
Owner

@PiefkePaul commented on GitHub (Sep 22, 2026):

Prepared this as an actual PR, but this repo's PR creation policy is collaborators-only, so I can't open one from a fork. The finished, tested diff is here if a maintainer wants to cherry-pick or merge it directly:

https://github.com/PiefkePaul/computer/compare/open-webui💻main...PiefkePaul💻fix/personal-chrome-deadlock-and-crop-alignment

Branch: PiefkePaul/computer@fix/personal-chrome-deadlock-and-crop-alignment (2 files changed, 16 insertions, 2 deletions — same two patches from the comments above, clean commit message with the same verification notes). Happy to be added as a collaborator to open it properly, or just let me know if you'd like anything about the diff adjusted.

<!-- gh-comment-id:5774797262 --> @PiefkePaul commented on GitHub (Sep 22, 2026): Prepared this as an actual PR, but this repo's PR creation policy is collaborators-only, so I can't open one from a fork. The finished, tested diff is here if a maintainer wants to cherry-pick or merge it directly: https://github.com/PiefkePaul/computer/compare/open-webui:computer:main...PiefkePaul:computer:fix/personal-chrome-deadlock-and-crop-alignment Branch: `PiefkePaul/computer@fix/personal-chrome-deadlock-and-crop-alignment` (2 files changed, 16 insertions, 2 deletions — same two patches from the comments above, clean commit message with the same verification notes). Happy to be added as a collaborator to open it properly, or just let me know if you'd like anything about the diff adjusted.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/computer#214
No description provided.