bbe4f21a3b
README: phone-as-second-screen section (build, install, sender usage, multi-monitor caveat). RUNBOOK: Android build/test procedure, device-test commands, and the API-36 platform quirks found during validation. PHASES: Phase 8 milestones, all validated. MEMORY: current phase state and the hard-won Android/portal lessons.
191 lines
7.7 KiB
Markdown
191 lines
7.7 KiB
Markdown
# screen_cast
|
|
|
|
A native Linux peer-to-peer screencast application.
|
|
|
|
- **Send** your desktop or a window to another Linux machine.
|
|
- **Receive** a stream and render it in a window — or fullscreen headless.
|
|
- Discover receivers on the LAN via **mDNS/Avahi**; negotiate sessions with
|
|
**JSON signaling**; stream **H.264 over RTP/UDP**.
|
|
- Control it from the **CLI**, a **GTK4 panel**, or a **waybar widget**.
|
|
|
|
Built with **C++20**, **Meson**, **PipeWire**, **FFmpeg**, and **SDL3**.
|
|
|
|
## Quick start
|
|
|
|
Requirements:
|
|
|
|
- C++20 compiler with `<format>` (GCC 13+, Clang 18+)
|
|
- Meson >= 0.63, Ninja
|
|
- FFmpeg development packages (`libavcodec`, `libavutil`, `libswscale`)
|
|
- SDL3 development package (`sdl3`)
|
|
- nlohmann JSON (`nlohmann_json`) and Avahi client (`avahi-client`)
|
|
- For the sender only: PipeWire dev (`libpipewire-0.3`) and libportal
|
|
- Optional, for the GUI: gtkmm-4.0
|
|
|
|
Build and run tests:
|
|
|
|
```sh
|
|
meson setup build
|
|
meson compile -C build
|
|
meson test -C build --print-errorlogs
|
|
```
|
|
|
|
Stream between two machines:
|
|
|
|
```sh
|
|
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.
|
|
|
|
## Modes and flags
|
|
|
|
```text
|
|
screencast --send [--target monitor|window] [--peer HOST[:PORT]] [--bitrate MAX_KBPS] [--crf 0-51] [--fps 1-60]
|
|
screencast --receive [--port PORT] [--signaling-port PORT] [--fullscreen] [--swdecode]
|
|
screencast --discover [--timeout SECONDS]
|
|
screencast waybar [--toggle] # for waybar widgets
|
|
```
|
|
|
|
| Mode | Flag | Default | Meaning |
|
|
|-------------|------------------|---------------|------------------------------------------------|
|
|
| `--send` | `--target` | `monitor` | `monitor` or `window` (portal source picker) |
|
|
| | `--peer` | auto-discover | target a receiver directly, `HOST[:PORT]` |
|
|
| | `--bitrate` | `4000` | VBV max bitrate (kbps) |
|
|
| | `--crf` | `22` | encoder quality (lower = better) |
|
|
| | `--fps` | capture rate | frame-rate cap, 1-60 |
|
|
| `--receive` | `--port` | `5004` | local RTP UDP port |
|
|
| | `--signaling-port` | `5005` | TCP signaling port announced via mDNS |
|
|
| | `--fullscreen` | off | force fullscreen (automatic under KMSDRM) |
|
|
| | `--swdecode` | off | skip the hardware decode probe |
|
|
| `--discover`| `--timeout` | `3` | seconds to wait for mDNS responses |
|
|
| `waybar` | `--toggle` | print status | toggle streaming (stop / restart last session) |
|
|
|
|
`--send` without `--peer` discovers receivers on the LAN and requires that
|
|
exactly one is found; use `--discover` to list them.
|
|
|
|
## GUI and waybar widget
|
|
|
|
Build the GUI alongside the CLI (`meson configure build -Dgui=true`, then
|
|
recompile; installs as `screencast-gui`):
|
|
|
|
```sh
|
|
screencast-gui # receiver list → pick one → bitrate → Start
|
|
screencast waybar # one JSON line for a waybar custom module
|
|
screencast waybar --toggle
|
|
```
|
|
|
|
The waybar module shows the app icon (dimmed while idle, highlighted while
|
|
streaming; tooltip: receiver, bitrate, elapsed); left-click toggles
|
|
streaming to the last receiver, right-click opens the panel. All front-ends
|
|
agree on state because the sender publishes it to
|
|
`$XDG_RUNTIME_DIR/screencast/sender.json`. The icon (from `screencast_icon/`)
|
|
is installed into the hicolor theme by `meson install`, used by the GTK
|
|
panel and the waybar CSS, and embedded in the SDL receiver window.
|
|
|
|
## Under the hood: resilience
|
|
|
|
The receiver absorbs loss in two stages and recovers actively:
|
|
|
|
1. **Jitter window**: RTP packets are re-ordered by sequence number in a
|
|
small buffer (16 packets / 60 ms), so Wi-Fi reordering is not misread as
|
|
loss. In-order streams release immediately (zero added latency).
|
|
2. **PLI feedback**: when a frame arrives genuinely damaged, the receiver
|
|
drops it and asks the sender for a keyframe over the signaling channel
|
|
(rate-limited to one request per 500 ms); the sender re-encodes a
|
|
keyframe immediately, so recovery takes one frame time.
|
|
|
|
**Hardware decode**: the receiver probes `h264_v4l2m2m` (the VideoCore path
|
|
on Raspberry Pi) and falls back to software automatically; `--swdecode`
|
|
forces software.
|
|
|
|
## 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`. It also
|
|
installs and enables a systemd service so the receiver starts at boot —
|
|
`systemctl status screencast-receiver` to check on it (stop with
|
|
`sudo systemctl stop screencast-receiver`; always SIGTERM, never `kill -9`,
|
|
or stale mDNS records linger).
|
|
|
|
On a headless console the receiver runs fullscreen automatically with
|
|
aspect-preserving letterboxing — no window manager needed (SDL3 KMSDRM; it
|
|
renders on its own VT, tty7, so `Ctrl+Alt+F1` returns to the console).
|
|
See `docs/RUNBOOK.md` for the headless setup details.
|
|
|
|
## Direct link: receiver as a Wi-Fi hotspot
|
|
|
|
The receiver can act as a Wi-Fi access point, so a sender connects to the
|
|
board directly with no router in between:
|
|
|
|
```sh
|
|
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 active the Pi is reachable at `10.42.0.1`; on the sender, join the
|
|
hotspot's Wi-Fi and run `screencast --send`.
|
|
|
|
## Android receiver: a phone as the second screen
|
|
|
|
A native receiver app (Kotlin, minSdk 30) lets a phone act as the second
|
|
receiver — and with a USB-C DisplayPort-alt-mode cable, the phone screen
|
|
mirrors straight to HDMI. The app speaks the same signaling + RTP protocol,
|
|
so the sender needs no changes.
|
|
|
|
```sh
|
|
cd android
|
|
gradle :app:assembleDebug # or :app:installDebug with a device attached
|
|
adb install app/build/outputs/apk/debug/app-debug.apk
|
|
```
|
|
|
|
Launch the app (it advertises `_screencast._tcp` and shows its IP + ports),
|
|
then on the sender:
|
|
|
|
```sh
|
|
screencast --send --peer <phone-ip>:5005
|
|
```
|
|
|
|
Notes:
|
|
|
|
- The sender downscales to the phone's display resolution automatically
|
|
(from the signaling answer).
|
|
- The phone renders letterboxed (fit-within), fullscreen, screen kept on.
|
|
- The app needs only the `INTERNET` permission; no camera/location.
|
|
- mDNS discovery works on the same L2 segment; across subnets use `--peer`
|
|
(the app prints its IP and the exact fallback command).
|
|
- `--target monitor` captures the portal's default output — on multi-monitor
|
|
Hyprland/GTK-portal setups that may not be the one you want; use
|
|
`--target window` and pick a window on the target display.
|
|
|
|
## Architecture
|
|
|
|
See `docs/ARCHITECTURE.md` for module boundaries and design rules.
|
|
|
|
## Roadmap
|
|
|
|
Development is split into phases in `docs/PHASES.md`.
|
|
|
|
Current phase: **Phase 8 — Android receiver app** (complete): the Kotlin
|
|
receiver app streams to a phone (validated on a Fairphone 6, phone → HDMI
|
|
via USB-C DP-alt-mode). Phase 7 items (PLI, jitter, hardware decode,
|
|
GUI/waybar) are complete and validated; deferred items remain the VAAPI
|
|
hardware encode probe and packaging.
|
|
|
|
## License
|
|
|
|
MIT — see `LICENSE` (to be added).
|