fix(cv2-capture): self-heal FrameSink capture across cross-doc nav RVH swap #38

Merged
triform-admin merged 1 commit from cv2-capture-rearm-land into main 2026-07-08 19:52:06 +00:00

The one true cold-boot frozen-video bug. Cold-boot cb-chromium guest arms FrameSink capture on about:blank; nav to real content swaps the RenderViewHost (+FrameSinkId) but the capturer is NEVER re-pointed at the new sink → RENDERER-STARVED, frames_encoded=0, frozen video. Warm-restore is immune (snapshotted already-on-content). CbActiveWebContentsResolver gains an optional SetRecaptureOnRvhSwapCallback; RenderViewHostChanged POSTs it; RearmCaptureAfterRvhSwap re-resolves WC→RWHV→RWH→GetFrameSinkId, forces view SHOWING, re-StartCapture (capturer.cc ChangeTarget re-points, verified not a no-op), bounded 5x/50ms retry. Adversarial review 8/8 SAFE (no UAF, no re-entrancy, threading OK). Optional callback → legacy invalidate-only when unset (tests unaffected).

Deploy: guest change → cr7727- build → cb-rootfs bake → isolator roll.

The one true cold-boot frozen-video bug. Cold-boot cb-chromium guest arms FrameSink capture on about:blank; nav to real content swaps the RenderViewHost (+FrameSinkId) but the capturer is NEVER re-pointed at the new sink → RENDERER-STARVED, frames_encoded=0, frozen video. Warm-restore is immune (snapshotted already-on-content). CbActiveWebContentsResolver gains an optional SetRecaptureOnRvhSwapCallback; RenderViewHostChanged POSTs it; RearmCaptureAfterRvhSwap re-resolves WC→RWHV→RWH→GetFrameSinkId, forces view SHOWING, re-StartCapture (capturer.cc ChangeTarget re-points, verified not a no-op), bounded 5x/50ms retry. Adversarial review 8/8 SAFE (no UAF, no re-entrancy, threading OK). Optional callback → legacy invalidate-only when unset (tests unaffected). Deploy: guest change → cr7727-<sha> build → cb-rootfs bake → isolator roll.
fix(cv2-capture): self-heal FrameSink capture across cross-doc nav RVH swap
Some checks failed
CI / Docs link check (pull_request) Failing after 2m4s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 2m3s
CI / Lint (pull_request) Failing after 2m8s
CI / Build container image (pull_request) Failing after 3m2s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 3m50s
CodeQL / Analyze go (pull_request) Failing after 3m52s
native-peer-gate / Build chromeless:ci for gate (pull_request) Failing after 4m40s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Has been skipped
0a39f4c1d0
THE root cause of cold-boot frozen video. A cold-boot cb-chromium guest
starts on about:blank; capture is armed on that page's FrameSinkId; then
the navigation to real content swaps the RenderViewHost (and its
FrameSinkId), but nothing re-points the capturer at the new sink. It
stays bound to the dead pre-nav sink -> VERDICT=RENDERER-STARVED,
frames_encoded=0, no video. Warm-restored golden sessions dodge this
(they resume already-on-content with no about:blank->nav transition),
which is why cold-boot was the sole frozen path -- and why the isolator
cold-boot fallback, though correct at the allocation layer, produced a
connected-but-frozen stream.

Fix: make capture self-healing at the layer that owns both the observer
and the capturer. CbActiveWebContentsResolver already observes the
captured WebContents and receives RenderViewHostChanged, but is
content-layer glue with no capturer handle (the FrameSink capturer is
deliberately content-agnostic -- FrameSinkIds only). So the resolver
gains an optional re-arm closure (SetRecaptureOnRvhSwapCallback) that
CloudBrowserBrowserMainParts supplies; on an RVH swap of the active
capture the resolver POSTs it (the new RWH's view/FSID may not be the
WC's live primary synchronously at RenderViewHostChanged -- a same-turn
UI PostTask runs after the swap settles). The owner
(RearmCaptureAfterRvhSwap) re-resolves WC->RWHV->RWH->GetFrameSinkId,
forces the fresh view SHOWING (the post-nav RWH boots HIDDEN -> renderer
throttled, same gating Cb.startFrameSinkCapture applies), calls
cb_track_source_->StartCapture on the new sink, and refreshes the
resolver's active target.

capturer.cc:138-164 confirms StartCapture on a NEW FrameSinkId re-targets
(ChangeTarget), not a no-op -- so the re-arm is effective. For a terminal
nav whose fresh RWHV isn't attached by the task turn (no later swap to
retrigger), the owner re-posts itself up to 5x/50ms (bounded) rather than
strand the capturer. Stale "Start is a no-op" comments in
cb_devtools_agent.cc + cb_framesink_video_track_source.{h,cc} that
predated the retarget path are corrected so this fix can't be mistakenly
reverted as dead code.

Layering preserved (capturer stays content-agnostic; re-arm lives in
build-integration where content deps already exist). Optional callback:
unset -> legacy invalidate-only behaviour, so tests/no-capturer builds
are unaffected. Adversarially reviewed (lifetime/UAF, re-entrancy,
threading, StartCapture-idempotency, includes, Show() walk) -- no
correctness bugs. Composes with the isolator cold-boot fallback (now a
COMPLETE fix, not frozen) and the per-language warm-slot fix.

Guest change -- needs a cb-chromium rootfs bake to deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
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
triform/chromeless!38
No description provided.