7 Commits

Author SHA1 Message Date
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
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
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
fegger 56e0d3662b build: survive low-memory boards in the receiver install script
The Raspberry Pi Zero 2 W (512 MB RAM) ran out of memory compiling
signaling.cpp — nlohmann/json peaks well above available RAM at -O3,
and ninja runs 6 parallel jobs on a quad-core — so the OOM killer
terminated the compiler with 'Killed signal terminated program
cc1plus'.

When less than 1.5 GB RAM is detected, the install script now adds
1 GB of temporary build-time swap (fallocate with a dd fallback,
removed on exit; some filesystems such as btrfs refuse swapfiles
outright, in which case it warns and continues) and compiles with a
single job. The cleanup trap also covers the SDL3 source directory.

The swap mechanics were live-tested with a small swapfile (swapon/
swapoff lifecycle); this dev machine's filesystem rejects swapfiles,
which is what exposed the need for the warning fallback. The Pi's
ext4 rootfs accepts them.
2026-09-07 13:20:14 +02:00
fegger 47905004eb build: make the receiver service headless-console ready
The receiver does not need a window manager: SDL3's KMSDRM backend
renders straight to the kernel display pipeline, which is the right
setup for a small-board appliance. But a systemd system service has
no controlling terminal, and SDL's KMSDRM backend expects one for
its VT handling — so the headless path would have failed at boot.
Give the unit StandardInput=tty with TTYPath=/dev/tty1 (harmless in
desktop mode), and document the headless recipe in the unit, the
install script output, and the RUNBOOK: Raspberry Pi OS Lite with
Console Autologin, no desktop enabled (a compositor would hold the
DRM master), and consoleblank=0.
2026-09-07 13:11:02 +02:00
fegger 6e68300d05 build: add systemd autostart for the receiver
Ship systemd/screencast-receiver.service, a template the install
script substitutes (binary path, run user) and enables so the
receiver starts at boot after the network and avahi-daemon, with
restart-on-failure and a raised start limit for slow-booting boards.
SIGTERM gives the graceful shutdown that withdraws the mDNS
announcement — systemctl stop never triggers the stale-record trap
that kill -9 does. Headless consoles render via KMSDRM (the script
adds the run user to video/render/input); desktop autologin sessions
uncomment two documented Environment lines.

The install script gains SC_RECEIVER_SERVICE=0 to opt out.

Validated: systemd-analyze verify on the substituted unit (only the
expected missing /usr/local/bin path on this dev machine), and a live
lifecycle test as a user unit: the service-managed receiver announced
itself (--discover found it), systemctl stop produced a clean exit
(Result=success, no restart) and the mDNS announcement was withdrawn
immediately.
2026-09-07 12:30:26 +02:00
fegger fb6a1087d5 build: add receiver-only configuration and a Pi install script
The receiver does not need the sender's PipeWire/xdg-desktop-portal
capture stack, which small ARM boards neither have nor want. Add a
-Dsender=false meson option that skips the capture backend and the
sender pipeline (SC_HAS_SENDER guards in main.cpp/pipelines.cpp);
receiver-only builds refuse --send with a clear message while
--receive and --discover work unchanged.

scripts/install-receiver.sh targets such boards (e.g. Raspberry Pi
Zero 2 W): installs build and runtime dependencies via apt, checks the
compiler for C++20 <format> support before the long build (GCC 13+,
i.e. Raspberry Pi OS Trixie), builds SDL3 from source when the distro
does not package it, compiles a receiver-only binary, runs the test
suite, installs to /usr/local/bin, and enables avahi-daemon.

Also refresh the stale README (phases, dependencies, current
roadmap, Pi receiver section).

Validated in both configurations: meson test 5/5 each; the
receiver-only build has no PipeWire/portal references, refuses --send
cleanly, and --discover works headlessly.
2026-09-07 12:24:57 +02:00