Files
screen_cast/docs/RUNBOOK.md
T
fegger 343c6b45e6 fix(systemd): give the receiver service its own VT instead of tty1
The receiver service never started on the Pi: systemd blocks before
exec in acquire_terminal(), waiting for a controlling terminal that
the console session or getty on tty1 already owns. Symptoms: the
status shows the main PID as "(screencast)" with Tasks:1 and ~40ms
CPU, a silent journal, and no mDNS announcement — the binary never
ran. Reproduced locally with a transient unit (the desktop's Wayland
session owns tty1) and diagnosed by gdb-attaching the stuck process.

The service now owns a dedicated free VT (tty7) so acquisition is
immediate, with a tolerant ExecStartPre chvt to make the screencast
the on-screen console at boot. tty1 keeps the console login;
Ctrl+Alt+F1/Ctrl+Alt+F7 switch between them. The previous advice to
enable Console Autologin actively caused the hang and is removed
from the unit, RUNBOOK, and install script notes.

Validated locally: the same properties via systemd-run give a
running receiver with all ~15 threads alive that announces itself
and is found by --discover; verified unit passes systemd-analyze
verify; graceful stop withdraws mDNS.
2026-09-07 13:45:47 +02:00

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

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). For very high --bitrate values, raise net.core.rmem_max on the receiver machine.

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