Files
screen_cast/docs/RUNBOOK.md
T
fegger 236584c240 build: raise net.core.rmem_max on receiver installs
UDP receive buffers clamp to net.core.rmem_max, whose stock Debian
default (~208 KB) is smaller than one VBV-bounded keyframe burst at
moderate-to-high bitrates. The resulting packet loss damages every
keyframe, drop-on-damage discards them silently, and the receiver
shows a black screen with no journal errors while the sender streams
happily. The install script now raises rmem_max to 4 MB and persists
the sysctl; buffers are allocated lazily, so this costs no RAM at
rest. Wi-Fi-only boards (Pi Zero 2 W) remain bandwidth-bound — the
RUNBOOK points at --bitrate as the tuning knob.
2026-09-07 13:59:56 +02:00

6.0 KiB

Runbook — build and validation commands

Reusable validation steps for this repository. Run the relevant ones before considering a change complete.

Build and test

meson setup build          # once
meson compile -C build
meson test -C build --print-errorlogs

Memory and correctness checks (codec changes)

Run the codec round-trip test under valgrind after touching the encoder or decoder:

valgrind --leak-check=full --errors-for-leak-kinds=definite \
    build/tests/test_codec_roundtrip

Expected: no "definitely lost" bytes and no "Invalid read/write" errors. FFmpeg may keep some "still reachable" allocations at exit; that is normal.

This check caught two real defects in the Phase 2 decoder: a per-packet payload leak and an unpadded extradata buffer over-read. Keep using it.

Capture smoke test (Phase 3, manual)

Requires a running desktop session with xdg-desktop-portal and a backend that implements the ScreenCast portal (e.g. Hyprland, GNOME, KDE). The portal shows an interactive source picker, so this cannot run unattended.

meson compile -C build                       # builds tools/capture_smoke
./build/tools/capture_smoke 10 out.h264      # captures 10 frames
ffprobe -v error -show_entries stream=codec_name,width,height out.h264

Expected: the tool prints the negotiated resolution/format/stride and writes a non-empty file; ffprobe reports codec_name=h264 and the correct width/height. The encoder prepends its Annex-B SPS/PPS extradata so the file is a self-contained elementary stream.

If you see impl_ext_end_proxy called from wrong context warnings, a PipeWire proxy operation ran without the thread-loop lock — all pw calls that send messages (connect, stream, core) must happen under pw_thread_loop_lock.

LAN discovery and signaling (Phase 6)

Two machines on the same LAN (mDNS via avahi-daemon, which must be running on both):

screencast --receive                # machine A: announce + window
screencast --send                   # machine B: discovers A, negotiates, streams
screencast --discover               # list receivers on the LAN

--send without --peer requires exactly one discovered receiver. Use --peer HOST[:PORT] to target a receiver directly (signaling port defaults to 5005), e.g. --peer 192.168.178.20.

Operational note: never kill -9 a running receiver — a SIGKILLed process cannot withdraw its mDNS registration, and avahi-daemon then keeps broken records for that service name until they expire (~75 min), making resolution time out for every peer. If that happens, systemctl restart avahi-daemon clears it. Always stop screencast with Ctrl-C (SIGTERM).

Receiver as a systemd service (autostart)

scripts/install-receiver.sh installs and enables screencast-receiver.service (see systemd/screencast-receiver.service). It starts at boot after the network and avahi-daemon, and restarts on failure. On a headless console SDL renders via KMSDRM; on a desktop autologin, edit /etc/systemd/system/screencast-receiver.service and uncomment the Environment lines for XDG_RUNTIME_DIR/WAYLAND_DISPLAY.

systemctl status  screencast-receiver          # is it running?
journalctl -u screencast-receiver -f           # follow logs
sudo systemctl stop screencast-receiver        # graceful; withdraws mDNS
sudo systemctl disable --now screencast-receiver  # turn autostart off

Verified behavior: systemctl stop results in a clean exit (success, no restart) and the mDNS announcement is withdrawn — senders stop finding the receiver immediately.

Headless receiver (no window manager)

The receiver does not need X11, Wayland, or any window manager: SDL3's KMSDRM backend renders straight to the kernel display pipeline. This is the recommended setup for small boards (Raspberry Pi OS Lite):

  1. No desktop may be enabled — a running compositor holds the DRM master and the receiver cannot take it (the two modes are mutually exclusive). No console autologin is needed either.
  2. The service renders on its own VT (tty7) and switches to it at boot: Ctrl+Alt+F1 returns to the console login, Ctrl+Alt+F7 back to the screencast. (A service must not target tty1: acquiring a tty that the console session or getty owns blocks the start forever.)
  3. Disable console blanking: append consoleblank=0 to /boot/firmware/cmdline.txt and reboot.

Under KMSDRM the receiver goes fullscreen automatically, with aspect-preserving letterboxing (a 4:3 desktop on a 16:9 TV shows black bars, not a stretched picture) and no mouse cursor. On desktop sessions --fullscreen forces the same behavior.

A window manager only comes with the desktop-session alternative, where the receiver runs inside it (uncomment the Environment= lines in the service file as described above).

Sender / receiver loopback (Phase 5, manual)

Two terminals on the same desktop session:

./build/src/app/screencast --receive        # terminal 1: window appears
./build/src/app/screencast --send           # terminal 2: portal source picker

Expected: the receiver window shows the captured desktop in near real time. If the receiver starts after the sender, a few non-existing PPS decode errors are normal until the next keyframe arrives (the GOP is ~0.4s); video should appear within a second. Stop either side with Ctrl-C. Optional flags: --port, --peer HOST[:PORT], --bitrate KBPS, --target window.

Notes: the encoder runs a VBV that caps keyframe bursts to roughly two frame periods of bytes, and the receiver requests a 4MB UDP receive buffer (clamped by net.core.rmem_max; the install script raises it to 4 MB and persists the change). The remaining bottleneck on Wi-Fi-only boards is the wireless link itself — lower --bitrate until the stream is stable.

The headless equivalent runs as part of meson test (udp loopback test): synthetic frames → encode → packetize → localhost UDP → depacketize → decode, no portal or window involved.

Formatting

find include src tests -type f \( -name '*.cpp' -o -name '*.h' \) \
    -exec clang-format --dry-run --Werror {} +