Screen stream (WebRTC): skip unchanged frames (Petal's dirty-rect gate), 1 s keep-alive
Leo, 2026-11-05: "honestly i'd rather us look at lot more at Petal and stuff like we ought to be able to get a lot more efficient and start going towards crazy stuff... after being able to do the basics end to end and adding eval measurements, ofc", and on the Petal list: "obv do all these and more".
HQ#7146 item 3 of the Petal ports (WebRTC path, CmuxVNCWebRTC): the static-screen half. The low-QP or lossless still once the screen goes static is a follow-up.
What changes
- Dirty-rect skip gate (
ScreenCaptureBackend). A complete ScreenCaptureKit frame whosedirtyRectsattachment is empty, at the same size and with the pointer where it was, no longer goes to the encoder. A missing attachment counts as changed (Petal's "unknown is always pushed"), and so do the first frame after a capture start, a size change and a pointer move (the cursor is drawn into the picture and may not count as dirty). Ported from Petal (https://github.com/kiruna-labs/petal @ 7ac436a, Apache-2.0,apps/desktop/src-tauri/src/session/share.rs:2060-2290). - Keep-alive once a second, not twice (
ScreenStreamHost). The idle refresh re-sent the last frame every 500 ms so a lost keyframe or a quality step had a frame to encode; it now does so after 1 s without a frame (Petal's static refresh floor). - The host's 3 s
video:log line addsunchanged-skipped N fps, so a run shows how often the gate fires.
Numbers
Before and after from screen-stream-g0's harness (scripts/measure/screen-stream-eval.sh, webrtc target; idle kbps and fps are the ones this should move, typing/scroll/latency should not get worse): pending. The harness is cmux#36043, not yet on feat-cmux-next. This PR stays a draft until the numbers are in.
Tests
FrameRateTests.unchangedFramesStayOutOfTheEncoder (new).
Observed / Not run
- Prepush gate: no new violations.
- Not run yet: the package tests on the fleet, and the harness.
Changelog
- Changed: a still Mac screen streamed to the iPhone sends fewer frames.
🤖 Generated with Claude Code
Agent session: agent session
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Skips unchanged frames in the WebRTC screen stream so a still Mac screen stops sending redundant frames, and refreshes it just enough to keep text sharp: every 250 ms for the first 2 s after the screen settles, then once a second (the 1 s floor alone blurred text, 17.8 % vs 11.2 % OCR error).
- Complete frames with zero dirty rects, the same size, and an unchanged pointer are dropped before the encoder; unknown dirty rects, the first frame after a capture start, size changes, pointer moves, and the frame after the pointer comes to rest still go through.
- Only the installed stream's frames pass; installing a stream resets the gate and held frame, so a new stream's first frame is never compared with an earlier one, and frames a stopping stream sends while it winds down are dropped.
- The backend keeps the newest skipped frame and the keep-alive sends it, so a cursor the gate missed shows instead of the last frame sent.
- The 3 s video log line now reports how many frames were skipped.