#!/usr/bin/env bash # # install-receiver.sh — build and install the screencast RECEIVER on a # small ARM board (e.g. a Raspberry Pi Zero 2 W). # # Why a separate script: the sender pulls in PipeWire and the # xdg-desktop-portal capture stack, which a headless receiver board neither # has nor needs. This script configures the build with -Dsender=false so # only the receiver binary is compiled. # # Usage (from the repository root, on the target machine): # # sudo ./scripts/install-receiver.sh # # Environment overrides: # SC_RECEIVER_INSTALL_DIR install target (default /usr/local/bin) # SC_RECEIVER_SKIP_TESTS set to 1 to skip the test suite (faster install) # SC_RECEIVER_SERVICE set to 0 to skip the systemd service setup # # Requirements: # - Debian/Raspberry Pi OS (apt) with GCC 13 or newer (the code uses # C++20 ; Raspberry Pi OS Trixie or newer ships a suitable GCC) # - a running avahi-daemon (installed and enabled by this script) # - a graphical session or KMS console for the receiver window set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BUILD_DIR="${REPO_ROOT}/build-receiver" INSTALL_DIR="${SC_RECEIVER_INSTALL_DIR:-/usr/local/bin}" SKIP_TESTS="${SC_RECEIVER_SKIP_TESTS:-0}" ENABLE_SERVICE="${SC_RECEIVER_SERVICE:-1}" log() { printf '\n=== %s\n' "$*"; } die() { printf 'install-receiver: error: %s\n' "$*" >&2; exit 1; } TEMP_SWAP="" SDL_DIR="" cleanup() { if [ -n "${TEMP_SWAP}" ]; then swapoff "${TEMP_SWAP}" 2>/dev/null || true rm -f "${TEMP_SWAP}" fi if [ -n "${SDL_DIR}" ]; then rm -rf "${SDL_DIR}" fi } trap cleanup EXIT [ "$(id -u)" = 0 ] || die "run with sudo (installs to ${INSTALL_DIR})" [ -f "${REPO_ROOT}/meson.build" ] || die "run from the screen_cast repository" command -v apt-get >/dev/null 2>&1 || die "this script supports apt-based systems only" export DEBIAN_FRONTEND=noninteractive # --- build and runtime dependencies ----------------------------------------- log "installing build and runtime dependencies (this may take a while)" apt-get update -qq || apt-get update apt-get install -y --no-install-recommends \ build-essential \ meson \ ninja-build \ pkg-config \ libavcodec-dev \ libavutil-dev \ libswscale-dev \ nlohmann-json3-dev \ libavahi-client-dev \ avahi-daemon \ ca-certificates # The receiver renders with SDL3. Older releases do not package it; build a # minimal SDL3 from source in that case. if ! apt-get install -y --no-install-recommends libsdl3-dev; then log "libsdl3-dev is not packaged; building SDL3 from source (15-30 min on small boards)" apt-get install -y --no-install-recommends cmake git SDL_DIR="$(mktemp -d)" git clone --depth 1 --branch release-3.2.x \ https://github.com/libsdl-org/SDL.git "${SDL_DIR}/SDL" cmake -S "${SDL_DIR}/SDL" -B "${SDL_DIR}/build" \ -DCMAKE_BUILD_TYPE=Release \ -DSDL_TESTS=OFF \ -DSDL_INSTALL_DOCS=OFF \ -DSDL_EXAMPLES=OFF cmake --build "${SDL_DIR}/build" --parallel "$(nproc)" cmake --install "${SDL_DIR}/build" fi # --- compiler sanity check --------------------------------------------------- log "checking the compiler for C++20 support" if ! printf '#include \nint main() { return 0; }\n' | g++ -std=c++20 -x c++ - -o /dev/null 2>/dev/null; then die "g++ lacks C++20 ; install GCC 13 or newer (Raspberry Pi OS \ Trixie or newer ships one). Aborting before a long build." fi # --- memory-constrained boards ---------------------------------------------- # The signaling unit includes nlohmann/json.hpp, which can peak well above # the RAM of small boards; the OOM killer then terminates the compiler # ("Killed signal terminated program cc1plus"). Give such boards temporary # swap and a single compile job. BUILD_JOBS="$(nproc)" MEM_TOTAL_KB="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)" if [ "${MEM_TOTAL_KB}" -lt 1500000 ]; then SWAP_TOTAL_KB="$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)" if [ "${SWAP_TOTAL_KB}" -lt 1000000 ]; then FREE_KB="$(df -k --output=avail / | awk 'NR==2 {print $1}')" if [ "${FREE_KB}" -lt 1572864 ]; then die "board has little RAM and not enough free disk for 1 GB of temporary build swap; build on a larger machine or add swap manually" fi log "low-memory board: adding 1 GB of temporary swap for the build (removed afterwards)" TEMP_SWAP="/var/tmp/screencast-build.swap" if ! (fallocate -l 1G "${TEMP_SWAP}" 2>/dev/null || dd if=/dev/zero of="${TEMP_SWAP}" bs=1M count=1024 status=none); then die "failed to write the swapfile" fi chmod 600 "${TEMP_SWAP}" if ! mkswap "${TEMP_SWAP}" >/dev/null 2>&1 || ! swapon "${TEMP_SWAP}" 2>/dev/null; then # Some filesystems (e.g. btrfs) refuse swapfiles outright. rm -f "${TEMP_SWAP}" TEMP_SWAP="" log "WARNING: could not activate a swapfile on this filesystem; continuing with a single compile job — the build may still run out of memory" fi fi BUILD_JOBS=1 log "low-memory board: compiling with a single job" fi # --- build the receiver ------------------------------------------------------ log "configuring a receiver-only build in ${BUILD_DIR}" rm -rf "${BUILD_DIR}" meson setup "${BUILD_DIR}" "${REPO_ROOT}" -Dsender=false log "compiling (this takes a while on small boards)" meson compile -C "${BUILD_DIR}" --jobs "${BUILD_JOBS}" if [ "${SKIP_TESTS}" != "1" ]; then log "running the test suite" meson test -C "${BUILD_DIR}" --print-errorlogs fi # --- install ----------------------------------------------------------------- log "installing the receiver to ${INSTALL_DIR}" install -m 755 "${BUILD_DIR}/src/app/screencast" "${INSTALL_DIR}/screencast" systemctl enable --now avahi-daemon 2>/dev/null || true # --- systemd autostart -------------------------------------------------------- if [ "${ENABLE_SERVICE}" = "1" ] && command -v systemctl >/dev/null 2>&1; then log "installing the systemd service (autostart on boot)" RUN_USER="${SUDO_USER:-root}" # Headless KMSDRM rendering needs device access; skip groups that do # not exist on this system. if [ "${RUN_USER}" != "root" ] && id "${RUN_USER}" >/dev/null 2>&1; then for group in video render input; do if getent group "${group}" >/dev/null 2>&1; then usermod -aG "${group}" "${RUN_USER}" || true fi done fi sed -e "s|__SC_RECEIVER_BIN__|${INSTALL_DIR}/screencast|g" \ -e "s|__SC_RECEIVER_USER__|${RUN_USER}|g" \ "${REPO_ROOT}/systemd/screencast-receiver.service" \ > /etc/systemd/system/screencast-receiver.service chmod 644 /etc/systemd/system/screencast-receiver.service systemctl daemon-reload systemctl enable --now screencast-receiver.service else log "skipping the systemd service (disabled or systemd not available)" fi log "done." cat <<'EOF' The receiver is installed and starts automatically at boot. systemctl status screencast-receiver # is it running? journalctl -u screencast-receiver -f # follow its logs sudo systemctl stop screencast-receiver # stop gracefully (Ctrl-C like) sudo systemctl disable --now screencast-receiver # turn autostart off Notes for small boards (e.g. Pi Zero 2 W): - No window manager is required: the receiver renders directly to the kernel display pipeline (SDL KMSDRM). Use Raspberry Pi OS Lite with Console Autologin (raspi-config) and no desktop enabled — a running compositor would own the display and the receiver could not start. - Disable console blanking so the picture never goes dark: add consoleblank=0 to /boot/firmware/cmdline.txt and reboot. - Decoding is software H.264; expect smooth playback for small streams and a lower frame rate at high resolutions. - Stop the receiver via systemctl (never kill -9; stale mDNS records would break discovery for ~75 minutes). - On a desktop session instead, edit the installed service file and uncomment the Environment lines for XDG_RUNTIME_DIR/WAYLAND_DISPLAY. - avahi-daemon must stay enabled; it is what senders discover. EOF