Cameras carry multiple stream URLs (streams: high/medium/low); each tile picks one via Quality (auto/high/low). Small tiles can pull the low substream to cut Pi decode load. discover --enable-rtsp takes a comma list (high,low), enables each channel, records all enabled ones. TUI: q cycles a tile quality (shown in the tile); discover enables high+low. Player: --framedrop + low-delay demuxer flags; player.max_fps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
217 lines
9.4 KiB
Markdown
217 lines
9.4 KiB
Markdown
# 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 <repo>/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
|
||
```
|