Files
screen_cast/docs/RUNBOOK.md
T
fegger 41f71fd217 feat(pi): add a Wi-Fi hotspot mode for routerless direct streaming
scripts/pi-hotspot.sh turns the receiver into a WPA2 access point via
NetworkManager (ipv4 shared mode gives the Pi built-in DHCP/NAT at
10.42.0.1), so a sender connects directly with no router in between —
which also sidesteps LAN quirks like unreachable 6to4 addresses, since
the direct link is plain private IPv4. The passphrase is generated on
first use and stored root-only in /etc/screencast-hotspot.conf;
on|off|status subcommands manage it, and off restores normal client
Wi-Fi. The hotspot owns wlan0 while active (documented). No receiver
changes were needed: it already announces on every interface.

nmcli property syntax validated against NetworkManager 1.58 with a
disposable never-activated profile; actual AP bring-up can only be
validated on the Pi.
2026-09-07 14:13:48 +02:00

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

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