# rtsp-streamer A full-screen RTSP video wall for UniFi Protect cameras, built for a Raspberry Pi 4 (or any Linux box) plugged into a TV. No Chrome kiosk, no desktop — just `mpv` tiles under a minimal Wayland compositor, driven by a single Go binary you configure over SSH from the command line or an interactive TUI. ## Why this design - **One `mpv` process per camera.** A dead or unreachable camera never takes down the wall — the daemon restarts only the slot that failed, with backoff. (Contrast with a single ffmpeg/`mpv` mosaic, where one stalled input can freeze the whole picture.) - **Compositor tiling, not fullscreen.** A tiny [sway](https://swaywm.org) session hosts the windows; the daemon positions each one at an exact pixel rectangle over sway's IPC. No X11, no browser, no GPU compositor tricks. - **Hardware decode.** `mpv --hwdec=auto-safe` uses the Pi 4's V4L2/DRM H.264 decoder. - **Stateful config.** A single hand-editable `config.yaml`, or edit it live with `rtsp-streamer tui`. Switching layouts is instant and doesn't restart anything you don't have to. ``` Pi 4 (headless, HDMI -> TV) ┌───────────────────────────────────────────────┐ │ tty1 autologin -> sway -> rtsp-streamer daemon │ │ ├─ reads active layout -> computes grid rects │ │ ├─ spawns 1 mpv per slot (--hwdec, IPC sock) │ │ ├─ swaymsg: float + position each to a cell │ │ └─ health-monitors mpv, restarts dead streams │ │ │ │ control socket ◀── rtsp-streamer layout set … │ │ ◀── rtsp-streamer tui (over SSH) │ └───────────────────────────────────────────────┘ ▲ discover UniFi Protect controller (login -> bootstrap -> RTSPS URLs) ``` ## Requirements On the display device: - `sway` and `mpv` - optional: a terminal like `foot` for the emergency keybinding Debian / Raspberry Pi OS: `sudo apt install sway mpv foot` Arch: `sudo pacman -S sway mpv foot` To build: **Go 1.23+**. You can build on your dev machine and copy the binary, or build on the Pi itself. > ⚠️ Debian / Raspberry Pi OS ships **Go 1.19** via `apt`, which is too old > (`log/slog` needs 1.21+). Install a current Go into `~/.local/go` and put it > first on `PATH` — do **not** rely on the system `go`: > ```sh > curl -sSLO https://go.dev/dl/go1.23.4.linux-arm64.tar.gz # match uname -m > rm -rf ~/.local/go && mkdir -p ~/.local && tar -C ~/.local -xzf go1.23.4.linux-arm64.tar.gz > echo 'export PATH=$HOME/.local/go/bin:$PATH' >> ~/.profile && export PATH=$HOME/.local/go/bin:$PATH > go version # must show 1.23+, not 1.19 > ``` ## Build ```sh make build # native binary -> bin/rtsp-streamer make pi # Raspberry Pi 4, 64-bit OS (arm64) -> bin/rtsp-streamer-arm64 make pi32 # 32-bit Pi OS (armv7) -> bin/rtsp-streamer-armv7 make test make install # build native + copy to /usr/local/bin (auto-sudo) make deploy # install + restart the kiosk session rtsp-streamer version # prints the baked-in git commit / build date ``` ## Configure ```sh rtsp-streamer config init # write a starter ~/.config/rtsp-streamer/config.yaml export RTSP_STREAMER_PASSWORD=… # controller password (kept out of the file) $EDITOR "$(rtsp-streamer config path)" # set controller.host / username rtsp-streamer discover # pull cameras + RTSPS URLs from UniFi Protect rtsp-streamer tui # assign cameras to layout slots, pick active ``` See [`config.example.yaml`](config.example.yaml) for the full schema. The config path is `$XDG_CONFIG_HOME/rtsp-streamer/config.yaml` (i.e. `~/.config/rtsp-streamer/config.yaml`), overridable with `--config` or `$RTSP_STREAMER_CONFIG`. ### Layouts (grid + spanning tiles) A layout is a base grid (`COLSxROWS`, up to 8×8) on which each camera is a **tile** that can span multiple cells. That covers a plain 2×2/3×3/4×4 as well as security-wall arrangements — one big main view plus a column of small ones, `2+6`, etc. A layout can show up to **16 cameras** (decoding more than that on a Pi 4 isn't practical even with substreams). Tiles must stay inside the grid and may not overlap. See `main-plus` in [`config.example.yaml`](config.example.yaml). ### The TUI `rtsp-streamer tui` is a Bubble Tea configurator meant to be run over SSH: - **Cameras** — review discovered cameras, `d` disable, `x` delete. - **Layouts** — edit an arrangement on a live ASCII grid preview. - **Set active layout** — choose what the wall shows. - **Discover** / **Discover + enable RTSP (high)** — pull cameras from Protect. - **Save** — writes the config and tells a running daemon to reload live. In the **layout editor** the grid is drawn live as you edit: ``` +----------+----------+----------+----------+ |Front Door| · | · |Driveway | |3x3 | · | · | | +----------+----------+----------+----------+ | · | · | · |Back Yard | ... ``` - move cursor: arrows or `h`/`j`/`k`/`l` - `enter` assign a camera to the cell (pick `(empty)` to clear) · `c` clear - resize the tile under the cursor: `L`/`H` wider/narrower, `J`/`K` taller/shorter - resize the base grid: `]`/`[` add/remove a column, `}`/`{` add/remove a row - `esc` back Elsewhere: arrows / `j`/`k` move, `enter` selects, `esc` goes back, `q` quits. ## Run ```sh rtsp-streamer daemon # normally launched by sway, not by hand rtsp-streamer status # slot health from the running daemon rtsp-streamer layout ls # list layouts (* marks active) rtsp-streamer layout set quad # switch live (persists the choice) ``` ## Deploy to a Pi (boot straight to the wall) ```sh make pi scp bin/rtsp-streamer-arm64 pi@display:/tmp/ # on the Pi, put the binary where install.sh expects it and run the installer: # mv /tmp/rtsp-streamer-arm64 /bin/rtsp-streamer sudo ./deploy/install.sh kiosk ``` `install.sh` installs the binary, drops the kiosk sway config at `/etc/rtsp-streamer/sway/config`, creates the `kiosk` user, enables tty1 autologin, and appends a launcher to the user's `~/.bash_profile` that starts sway on the physical console only (SSH sessions still get a normal shell). Reboot and the wall comes up. Provide the controller password to the daemon by sourcing an env file from the kiosk user's `~/.bash_profile` **before** the sway launcher, e.g.: ```sh # ~/.bash_profile (kiosk user) [ -f ~/.config/rtsp-streamer/env ] && . ~/.config/rtsp-streamer/env # >>> rtsp-streamer kiosk launcher >>> (added by install.sh) ``` ## Enabling RTSP in UniFi Protect Protect does not expose RTSP until you turn it on **per camera**. You can do it by hand (Protect → camera → Settings → Advanced → **RTSP**, enable a channel), or let rtsp-streamer flip it on for you via the API: ```sh rtsp-streamer discover --enable-rtsp=high # enable the High channel where missing rtsp-streamer discover --enable-rtsp=low # or the Low substream ``` In the TUI, the menu item **"Discover + enable RTSP (high)"** does the same. Enabling preserves each channel's other encoder settings — it only flips the `isRtspEnabled` flag. Plain `discover` (no flag) never modifies your controller; it just skips cameras with no RTSP-enabled channel and reports which ones. Use `discover --low-res` (or `--enable-rtsp=low`) to prefer substreams for dense grids (3x3+), which greatly cuts Pi decode load. ## Troubleshooting - **Blank/black tile** — verify the stream directly: `mpv --rtsp-transport=tcp 'rtsps://HOST:7441/ALIAS?enableSrtp'`. If that fails, RTSP probably isn't enabled for that camera (see above). - **`swaymsg not found` / windows not placed** — the daemon must run inside a sway session (started by the kiosk launcher). Check `echo $SWAYSOCK`. - **High CPU / dropped frames on a 3x3** — switch those cameras to substreams (`discover --low-res`) and/or add `player.extra_args: ["--vf=fps=15"]`. - **`login failed`** — use a *local* Protect account (not a Ubiquiti SSO login) and confirm `controller.host` reaches the UniFi OS console. ## Caveats & scope - **Unofficial API.** UniFi Protect has no public API; the local endpoints used here (`/api/auth/login`, `/proxy/protect/api/bootstrap`) are the same stable ones the Home Assistant integration relies on, but Ubiquiti could change them. Manual RTSP URLs in `cameras:` work with any camera brand and need no controller. - **Linux/Wayland only.** "Other platforms" means other Linux devices (x86 mini PCs, other SBCs) — anywhere sway + mpv run. It is not a macOS/Windows app. - **First cut** ships preset grids. Auto-cycling/rotation and free-form positioning are natural next steps (the daemon already re-tiles on demand). ## Layout ``` cmd/rtsp-streamer/ CLI (cobra): daemon, discover, layout, status, tui, config internal/config/ YAML load/save/validate, schema, XDG paths internal/protect/ UniFi Protect client (login, bootstrap, RTSPS URLs) internal/player/ one supervised mpv per stream, JSON IPC, restart/backoff internal/compositor/ sway control + NxM grid geometry internal/daemon/ orchestrator + control socket + health loop internal/ipc/ control-socket protocol shared by daemon and CLI/TUI internal/tui/ Bubble Tea configurator deploy/ sway kiosk config, autologin, install.sh ```