Files
screen_cast/docs/RUNBOOK.md
T

7.8 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.

The receiver can run as a Wi-Fi access point, so a sender connects to the Pi directly with no router in between (on such a direct link the Pi is plain private IPv4 — no 6to4 or router quirks):

sudo scripts/pi-hotspot.sh on            # prints SSID + generated password
sudo scripts/pi-hotspot.sh status         # shows the saved credentials
sudo scripts/pi-hotspot.sh off            # back to normal router Wi-Fi

While the hotspot is active it owns the Wi-Fi interface: the Pi leaves any router network, and the Pi is reachable at 10.42.0.1 (SSH included). On the sender, join the hotspot's Wi-Fi and run screencast --send — discovery and streaming then flow over the direct link. The passphrase is generated on first use and stored in /etc/screencast-hotspot.conf (root only). Override with --ssid, --psk, or --band a (5 GHz).

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).

Under the hood: resilience (Phase 7)

The receiver absorbs loss in two stages and recovers actively:

  1. Jitter window: RTP packets are re-ordered by sequence number in a small buffer (16 packets / 60 ms), so Wi-Fi reordering is not misread as loss. In-order streams release immediately (zero added latency).
  2. PLI feedback: when a frame arrives genuinely damaged, the receiver drops it and asks the sender for a keyframe over the signaling channel (rate-limited to one request per 500 ms). The sender re-encodes a keyframe immediately — recovery takes one frame time instead of waiting out the rest of the GOP.

Hardware decode: the receiver probes h264_v4l2m2m (the VideoCore path on Raspberry Pi) and falls back to software automatically; the journal says which path is active. --swdecode forces software.

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 {} +