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.
This commit is contained in:
+14
-3
@@ -23,6 +23,14 @@ loopback; current phase is Phase 7.
|
|||||||
both 127.0.0.1 and ::1.
|
both 127.0.0.1 and ::1.
|
||||||
- **Discovery dedupe**: one entry per (service_name, signaling_port) — a
|
- **Discovery dedupe**: one entry per (service_name, signaling_port) — a
|
||||||
host with many interfaces otherwise registers dozens of address variants.
|
host with many interfaces otherwise registers dozens of address variants.
|
||||||
|
- **Receiver-only builds** (`-Dsender=false`, meson option): skip the
|
||||||
|
capture backend and sender pipeline (`SC_HAS_SENDER` guards in
|
||||||
|
main.cpp/pipelines.cpp; `--send` refuses cleanly in such builds).
|
||||||
|
`scripts/install-receiver.sh` uses this to install on small ARM boards
|
||||||
|
(user's Raspberry Pi Zero 2 W receiver) without PipeWire/portal deps,
|
||||||
|
building SDL3 from source when the distro lacks it. Requires GCC 13+
|
||||||
|
(`<format>`). Validated on x86_64: builds, tests 5/5, --discover works,
|
||||||
|
--send refuses with a message.
|
||||||
- **mDNS operational lesson (hit during validation)**: `kill -9` on a
|
- **mDNS operational lesson (hit during validation)**: `kill -9` on a
|
||||||
process holding an avahi registration leaves stale daemon records; the
|
process holding an avahi registration leaves stale daemon records; the
|
||||||
service then browses but never resolves (timeout for every peer) until
|
service then browses but never resolves (timeout for every peer) until
|
||||||
@@ -117,9 +125,12 @@ None.
|
|||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
- GUI framework (Qt6 vs. none / CLI only) — deferred to later phase.
|
- GUI framework (Qt6 vs. none / CLI only) — deferred to later phase.
|
||||||
- Hardware acceleration strategy (VAAPI / Vulkan Video / NVENC) — evaluate after
|
- Hardware acceleration strategy (VAAPI / Vulkan Video / NVENC) — evaluate
|
||||||
software encode path works.
|
after software encode works; note the Pi Zero 2 W receiver use case makes
|
||||||
- IPv6 at the transport layer — revisit when LAN streaming lands (Phase 6+).
|
hardware **decode** (V4L2/MMAL) the more urgent half.
|
||||||
|
- IPv6 at the transport layer — RESOLVED in Phase 6: dual-stack everywhere
|
||||||
|
(AF_UNSPEC resolution, IPV6_V6ONLY=0 listeners); validated over both
|
||||||
|
families.
|
||||||
|
|
||||||
## Forward-looking review notes (for later phases)
|
## Forward-looking review notes (for later phases)
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ A native Linux peer-to-peer screencast application.
|
|||||||
|
|
||||||
- **Send** your desktop or a window to another Linux machine.
|
- **Send** your desktop or a window to another Linux machine.
|
||||||
- **Receive** a stream and render it in a window.
|
- **Receive** a stream and render it in a window.
|
||||||
- Built with **C++20**, **Meson**, **PipeWire**, **FFmpeg**, **RTP/UDP**, and
|
- Discover receivers on the LAN via **mDNS/Avahi**; negotiate sessions with
|
||||||
**SDL**.
|
**JSON signaling**; stream **H.264 over RTP/UDP**.
|
||||||
|
|
||||||
> This project is in early development. The H.264 codec path is implemented;
|
Built with **C++20**, **Meson**, **PipeWire**, **FFmpeg**, and **SDL3**.
|
||||||
> capture, transport, and rendering are still in progress.
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- C++20 compiler (GCC 12+, Clang 16+)
|
- C++20 compiler with `<format>` (GCC 13+, Clang 18+)
|
||||||
- Meson >= 0.63
|
- Meson >= 0.63, Ninja
|
||||||
- Ninja
|
|
||||||
- FFmpeg development packages (`libavcodec`, `libavutil`, `libswscale`)
|
- FFmpeg development packages (`libavcodec`, `libavutil`, `libswscale`)
|
||||||
- (later phases) PipeWire dev, SDL2/3 dev
|
- SDL3 development package (`sdl3`)
|
||||||
|
- nlohmann JSON (`nlohmann_json`) and Avahi client (`avahi-client`)
|
||||||
|
- For the sender only: PipeWire dev (`libpipewire-0.3`) and libportal
|
||||||
|
|
||||||
Build and run tests:
|
Build and run tests:
|
||||||
|
|
||||||
@@ -28,12 +28,35 @@ meson compile -C build
|
|||||||
meson test -C build --print-errorlogs
|
meson test -C build --print-errorlogs
|
||||||
```
|
```
|
||||||
|
|
||||||
Run with verbose test output:
|
Stream between two machines:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
meson test -C build -v
|
screencast --receive # machine A: announces itself, opens a window
|
||||||
|
screencast --send # machine B: discovers A, negotiates, streams
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See `docs/RUNBOOK.md` for all modes, flags, and validation procedures.
|
||||||
|
|
||||||
|
## Receiver on a small ARM board (e.g. Raspberry Pi Zero 2 W)
|
||||||
|
|
||||||
|
The receiver does not need the sender's PipeWire/portal capture stack. On
|
||||||
|
the board, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ./scripts/install-receiver.sh
|
||||||
|
screencast --receive
|
||||||
|
```
|
||||||
|
|
||||||
|
The script installs the dependencies, builds a receiver-only binary
|
||||||
|
(`-Dsender=false`), runs the test suite, and installs to `/usr/local/bin`
|
||||||
|
(override with `SC_RECEIVER_INSTALL_DIR`). It needs Raspberry Pi OS Trixie
|
||||||
|
or newer (GCC 13+ for C++20 `<format>`), builds SDL3 from source when the
|
||||||
|
distribution does not package it, and enables `avahi-daemon`.
|
||||||
|
|
||||||
|
Performance note: decoding is software H.264; on very small boards expect
|
||||||
|
smooth playback for modest resolutions and reduced frame rates at high
|
||||||
|
resolutions. Hardware decode is planned for Phase 7.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
See `docs/ARCHITECTURE.md` for module boundaries and design rules.
|
See `docs/ARCHITECTURE.md` for module boundaries and design rules.
|
||||||
@@ -42,7 +65,7 @@ See `docs/ARCHITECTURE.md` for module boundaries and design rules.
|
|||||||
|
|
||||||
Development is split into phases in `docs/PHASES.md`.
|
Development is split into phases in `docs/PHASES.md`.
|
||||||
|
|
||||||
Current phase: **Phase 3 — PipeWire screen capture**.
|
Current phase: **Phase 7 — Resilience and polish**.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -10,8 +10,10 @@ project('screen_cast', 'cpp',
|
|||||||
# Public and private include directories are declared in `src/meson.build`.
|
# Public and private include directories are declared in `src/meson.build`.
|
||||||
subdir('src')
|
subdir('src')
|
||||||
|
|
||||||
# Manual smoke tools
|
# Manual smoke tools are sender-side.
|
||||||
subdir('tools')
|
if get_option('sender')
|
||||||
|
subdir('tools')
|
||||||
|
endif
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
enable_tests = get_option('tests')
|
enable_tests = get_option('tests')
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ option('tests', type : 'boolean', value : true,
|
|||||||
|
|
||||||
option('gui', type : 'boolean', value : false,
|
option('gui', type : 'boolean', value : false,
|
||||||
description : 'Build the optional GUI target (deferred to a later phase)')
|
description : 'Build the optional GUI target (deferred to a later phase)')
|
||||||
|
|
||||||
|
option('sender', type : 'boolean', value : true,
|
||||||
|
description : 'Build the sender; requires the PipeWire and xdg-desktop-portal development packages. Disable for receiver-only targets such as small ARM boards.')
|
||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/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)
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Debian/Raspberry Pi OS (apt) with GCC 13 or newer (the code uses
|
||||||
|
# C++20 <format>; 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}"
|
||||||
|
|
||||||
|
log() { printf '\n=== %s\n' "$*"; }
|
||||||
|
die() { printf 'install-receiver: error: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[ "$(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)"
|
||||||
|
trap 'rm -rf "${SDL_DIR}"' EXIT
|
||||||
|
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 <format> support"
|
||||||
|
if ! printf '#include <format>\nint main() { return 0; }\n' |
|
||||||
|
g++ -std=c++20 -x c++ - -o /dev/null 2>/dev/null; then
|
||||||
|
die "g++ lacks C++20 <format>; install GCC 13 or newer (Raspberry Pi OS \
|
||||||
|
Trixie or newer ships one). Aborting before a long build."
|
||||||
|
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 few minutes on small boards)"
|
||||||
|
meson compile -C "${BUILD_DIR}"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
log "done."
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
The receiver is installed. On this machine run:
|
||||||
|
|
||||||
|
screencast --receive
|
||||||
|
|
||||||
|
and on the sending machine:
|
||||||
|
|
||||||
|
screencast --send # discovers this receiver on the LAN
|
||||||
|
|
||||||
|
Notes for small boards (e.g. Pi Zero 2 W):
|
||||||
|
- Decoding is software H.264; expect smooth playback for small streams
|
||||||
|
and a lower frame rate at high resolutions.
|
||||||
|
- Stop the receiver with Ctrl-C; never kill -9 (it would leave stale
|
||||||
|
mDNS records that break discovery for ~75 minutes).
|
||||||
|
- The receiver needs avahi-daemon (enabled) and a graphical session or
|
||||||
|
KMS console to open its window.
|
||||||
|
EOF
|
||||||
@@ -31,6 +31,8 @@ void handle_interrupt(int) {
|
|||||||
g_interrupted.store(true);
|
g_interrupted.store(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SC_HAS_SENDER
|
||||||
|
|
||||||
sc::Endpoint parse_endpoint(std::string_view address, std::uint16_t default_port) {
|
sc::Endpoint parse_endpoint(std::string_view address, std::uint16_t default_port) {
|
||||||
sc::Endpoint endpoint;
|
sc::Endpoint endpoint;
|
||||||
const std::size_t separator = address.rfind(':');
|
const std::size_t separator = address.rfind(':');
|
||||||
@@ -47,6 +49,8 @@ sc::Endpoint parse_endpoint(std::string_view address, std::uint16_t default_port
|
|||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // SC_HAS_SENDER
|
||||||
|
|
||||||
std::string make_session_id() {
|
std::string make_session_id() {
|
||||||
static std::mt19937 engine{std::random_device{}()};
|
static std::mt19937 engine{std::random_device{}()};
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -97,6 +101,8 @@ std::vector<sc::DiscoveredPeer> discover_peers(int timeout_seconds, std::string&
|
|||||||
return peers;
|
return peers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SC_HAS_SENDER
|
||||||
|
|
||||||
int run_sender(const sc::SendCommand& command) {
|
int run_sender(const sc::SendCommand& command) {
|
||||||
// 1. Find the receiver's signaling endpoint: explicit --peer, or discover
|
// 1. Find the receiver's signaling endpoint: explicit --peer, or discover
|
||||||
// exactly one receiver on the LAN.
|
// exactly one receiver on the LAN.
|
||||||
@@ -202,6 +208,8 @@ int run_sender(const sc::SendCommand& command) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // SC_HAS_SENDER
|
||||||
|
|
||||||
int run_receiver(const sc::ReceiveCommand& command) {
|
int run_receiver(const sc::ReceiveCommand& command) {
|
||||||
sc::ReceiverPipelineConfig config;
|
sc::ReceiverPipelineConfig config;
|
||||||
config.local_rtp_endpoint = sc::Endpoint{"0.0.0.0", static_cast<std::uint16_t>(command.local_rtp_port)};
|
config.local_rtp_endpoint = sc::Endpoint{"0.0.0.0", static_cast<std::uint16_t>(command.local_rtp_port)};
|
||||||
@@ -247,7 +255,13 @@ int main(int argc, char* argv[]) {
|
|||||||
std::signal(SIGTERM, handle_interrupt);
|
std::signal(SIGTERM, handle_interrupt);
|
||||||
|
|
||||||
if (const sc::SendCommand* send = std::get_if<sc::SendCommand>(&*command)) {
|
if (const sc::SendCommand* send = std::get_if<sc::SendCommand>(&*command)) {
|
||||||
|
#ifdef SC_HAS_SENDER
|
||||||
return run_sender(*send);
|
return run_sender(*send);
|
||||||
|
#else
|
||||||
|
(void)send;
|
||||||
|
std::cerr << "screencast: this is a receiver-only build; --send is not available\n";
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
if (const sc::ReceiveCommand* receive = std::get_if<sc::ReceiveCommand>(&*command)) {
|
if (const sc::ReceiveCommand* receive = std::get_if<sc::ReceiveCommand>(&*command)) {
|
||||||
return run_receiver(*receive);
|
return run_receiver(*receive);
|
||||||
|
|||||||
+21
-7
@@ -1,9 +1,23 @@
|
|||||||
# The screencast application binary wiring every module together.
|
# The screencast application binary wiring every module together.
|
||||||
|
# Receiver-only builds (-Dsender=false) exclude the capture backend and the
|
||||||
|
# sender pipeline.
|
||||||
|
|
||||||
executable('screencast',
|
screencast_sources = [
|
||||||
[
|
'cli.cpp',
|
||||||
'cli.cpp',
|
'main.cpp',
|
||||||
'main.cpp',
|
'pipelines.cpp',
|
||||||
'pipelines.cpp',
|
]
|
||||||
],
|
|
||||||
dependencies : [sc_capture_dep, sc_codec_dep, sc_network_dep, sc_render_dep])
|
screencast_dependencies = [sc_codec_dep, sc_network_dep, sc_render_dep]
|
||||||
|
screencast_arguments = []
|
||||||
|
|
||||||
|
if build_sender
|
||||||
|
screencast_dependencies += [sc_capture_dep]
|
||||||
|
screencast_arguments += ['-DSC_HAS_SENDER=1']
|
||||||
|
endif
|
||||||
|
|
||||||
|
screencast = executable('screencast',
|
||||||
|
screencast_sources,
|
||||||
|
cpp_args : screencast_arguments,
|
||||||
|
dependencies : screencast_dependencies,
|
||||||
|
install : true)
|
||||||
@@ -28,6 +28,8 @@ std::string receiver_service_name() {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#ifdef SC_HAS_SENDER
|
||||||
|
|
||||||
class SenderPipeline::Impl {
|
class SenderPipeline::Impl {
|
||||||
public:
|
public:
|
||||||
explicit Impl(SenderPipelineConfig config) : config_(std::move(config)) {}
|
explicit Impl(SenderPipelineConfig config) : config_(std::move(config)) {}
|
||||||
@@ -121,6 +123,8 @@ void SenderPipeline::stop() {
|
|||||||
impl_->stop();
|
impl_->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // SC_HAS_SENDER
|
||||||
|
|
||||||
class ReceiverPipeline::Impl {
|
class ReceiverPipeline::Impl {
|
||||||
public:
|
public:
|
||||||
explicit Impl(ReceiverPipelineConfig config) : config_(std::move(config)) {}
|
explicit Impl(ReceiverPipelineConfig config) : config_(std::move(config)) {}
|
||||||
|
|||||||
+6
-1
@@ -1,7 +1,12 @@
|
|||||||
# Core public headers / include dependency.
|
# Core public headers / include dependency.
|
||||||
sc_core_inc = include_directories('../include')
|
sc_core_inc = include_directories('../include')
|
||||||
|
|
||||||
subdir('capture')
|
# The sender needs the PipeWire / xdg-desktop-portal capture backend;
|
||||||
|
# receiver-only builds skip it entirely.
|
||||||
|
build_sender = get_option('sender')
|
||||||
|
if build_sender
|
||||||
|
subdir('capture')
|
||||||
|
endif
|
||||||
|
|
||||||
# Phase 2 codec dependencies.
|
# Phase 2 codec dependencies.
|
||||||
dep_avcodec = dependency('libavcodec')
|
dep_avcodec = dependency('libavcodec')
|
||||||
|
|||||||
Reference in New Issue
Block a user