--- id: "2b6dcf53-6809-47b9-8643-0ef9f59cfd5f" name: "CDP screencast recording to mp4" micro_summary: "Tab-scoped browser video: Page.startScreencast(jpeg) frames auto-land in drain_events() (the daemon taps ALL CDP events into a deque, BUF=500); ack each frame, save JPEGs with receipt timestamps, then ffmpeg-concat with REAL per-frame durations to an mp4. No desktop/other-tab leak." status: "resolved" see_also: [] scope: "How to record a tab-scoped video of browser activity via CDP screencast and encode it to an mp4, without capturing the desktop or other tabs. Domain-agnostic browser-harness capability." warnings: "The page usually goes static BEFORE the URL commits, so the dwell captures 0 new frames — the fully-loaded page is the LAST frame of the segment; HOLD it for the payoff rather than expecting dwell frames. Drain every ~0.05s and call Network.disable during capture so screencast frames aren't dropped from the BUF=500 event buffer under load." --- ## Mechanism The daemon taps every CDP event into a `deque(maxlen=500)`; `drain_events()` returns+clears it. So `Page.startScreencast` frames flow into `drain_events()` — no daemon changes needed. This captures ONLY the page (unlike macOS screen recording), so nothing else on screen leaks. ## Recipe (capture thread) ```python sess = switch_tab(tab) # activates + binds daemon session cdp("Network.disable", session_id=sess) # cut event-buffer noise so frames aren't dropped drain_events() # clear backlog cdp("Page.startScreencast", session_id=sess, format="jpeg", quality=70, maxWidth=1400, maxHeight=850, everyNthFrame=1) # loop every ~0.05s: for e in drain_events(): if e["method"] != "Page.screencastFrame": continue p = e["params"]; save(base64.b64decode(p["data"])) frames.append((time.monotonic(), path)) # receipt time = frame time cdp("Page.screencastFrameAck", session_id=sess, sessionId=p["sessionId"]) # REQUIRED or Chrome stops ``` ## Encode (real timing, not fixed fps) Screencast is change-driven (static page => no frames), so build an ffmpeg concat list using the real gap between receipt timestamps as each frame's `duration`. This makes playback speed match reality. Even dimensions for yuv420p via `scale=trunc(iw/2)*2:trunc(ih/2)*2`. ## Reference scripts (tracked, off /tmp) - Capture-thread instance: `agent-workspace/so1024_demo.py` - Encoder (adds titles+chapters): child node "Title cards and chapters"; script `agent-workspace/screencast_encode.py` Run via `browser-harness -c "$(cat .../so1024_demo.py)"`. --- ### 2026-08-15 01:34 UTC ## 2026-08-15 — supported recording workflow For ordinary recording/video requests, use the built-in `start_recording(name, title=...)` / `stop_recording()` workflow and retain the exact returned directory. Auto-recording preferences are managed by `browser-harness recordings enable|disable`; `BH_RECORD=1|0` overrides one process. Use `browser-harness recordings --latest` only for a later request when timestamps and pages match, and never reenact completed work. Follow `interaction-skills/make-video.md` for post-production. The low-level screencast recipe above remains a specialized implementation reference, but its `browser-harness -c` launch examples are historical in v0.1.8; pass scripts through stdin instead. Its repository `agent-workspace/` script paths are also historical unless `BH_AGENT_WORKSPACE` explicitly points there; the default workspace is `~/.config/browser-harness/agent-workspace/`.