Screen stream (WebRTC): skip unchanged frames (Petal's dirty-rect gate), 1 s keep-alive

Draft
TE
3h ago
+178-10

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 whose dirtyRects attachment 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 adds unchanged-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
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.