Document the grid+tiles model, mouse/quality editing, git-based deploy (run as your user, reboot not getty), Pi 4 hwdec/H.265/substream notes, and the real troubleshooting lessons. Add a Roadmap incl. importing pre-made UniFi Protect views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
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
mpvprocess 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/mpvmosaic, where one stalled input can freeze the whole picture.) - Compositor tiling, not fullscreen. A tiny sway 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-safeuses the Pi 4's V4L2/DRM H.264 decoder. - Stateful config. A single hand-editable
config.yaml, or edit it live withrtsp-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:
swayandmpv- optional: a terminal like
footfor 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/slogneeds 1.21+). Install a current Go into~/.local/goand put it first onPATH— do not rely on the systemgo: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
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
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 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.
The TUI
rtsp-streamer tui is a Bubble Tea configurator meant to be run over SSH:
- Cameras — review discovered cameras,
ddisable,xdelete. - 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 |
...
- mouse (over SSH): click a cell to assign a camera; press on a tile and drag to resize its span, tmux-style. Click to select on the menus/lists too.
- move cursor: arrows or
h/j/k/l enterassign a camera to the cell (pick(empty)to clear) ·cclearqcycle this tile's stream quality (auto → high → low …); the choice is shown in the tile. Small tiles → low substream, big tiles → high.- resize the tile under the cursor:
L/Hwider/narrower,J/Ktaller/shorter - resize the base grid:
]/[add/remove a column,}/{add/remove a row escback
Elsewhere: arrows / j/k move (or click), enter selects, esc back, q quits.
Stream quality per tile
Discovery records every RTSP-enabled channel per camera (streams: {high, low, …}),
and each tile picks which to pull. Put big tiles on high and small tiles on the
low substream — that's the main lever for keeping a dense wall smooth on a Pi 4.
Enable both channels at once with discover --enable-rtsp=high,low (or the TUI's
"Discover + enable RTSP (hi+lo)").
Run
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)
rtsp-streamer version # baked-in git commit / build date
Deploy to a Pi (boot straight to the wall)
First-time setup (run install.sh as the user you'll run the wall as — pass
your own username; letting it default to a kiosk user is a common footgun,
because the daemon's control socket lives in that user's runtime dir and must
match your SSH user):
# get the binary onto the Pi — build there (needs Go 1.23, see Build), or copy:
git clone <repo> ~/RTSP-Streamer && cd ~/RTSP-Streamer && make build
sudo ./deploy/install.sh "$USER"
install.sh installs the binary to /usr/local/bin, drops the sway config at
/etc/rtsp-streamer/sway/config, adds you to the video/render/input groups,
enables tty1 autologin, and appends a launcher to ~/.bash_profile that
starts sway on the physical console only (SSH still gets a normal shell). The
running wall needs no controller password — the config already holds the
resolved stream URLs; the password is only for discover.
Then sudo reboot and the wall comes up on boot.
Updating
git pull && make install && sudo reboot
make install builds natively and copies to /usr/local/bin (auto-sudo — run
it without sudo so go build keeps your PATH). Use sudo reboot, not
systemctl restart getty@tty1: a getty restart tends to leave the old sway +
daemon running alongside the new one (duplicate mpv, windows fighting). Confirm
what's live with rtsp-streamer version.
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:
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.
Performance & latency (Pi 4)
The Pi 4 hardware-decodes H.264 only (no HEVC). If streams lag or CPU is pegged, work through:
- Confirm hardware decode is on. Probe a running player:
S=$XDG_RUNTIME_DIR/rtsp-streamer/mpv-slot-0.sock printf '{"command":["get_property_string","hwdec-current"]}\n' | socat - "$S" printf '{"command":["get_property_string","video-codec"]}\n' | socat - "$S"hwdec-current: nomeans software decoding.--hwdec=auto-safeoften won't pick the Pi's decoder — setplayer.hwdec: v4l2m2m-copyexplicitly. - H.265 cameras can't hardware-decode on a Pi 4. If
video-codecishevc, set that camera to H.264 in Protect, or use its (often H.264) low substream. - Use substreams for dense walls. Full-res (4K/4MP) × many tiles exceeds the
decoder's budget → fallback to software → growing latency. Give small tiles
the
lowquality (per-tileqin the TUI, orquality: lowin config). - Latency drift is handled by
--framedrop=decoder+vo+ low-delay demuxer flags (built in), so a briefly-behind stream drops frames to catch up instead of accumulating a backlog. Cappingplayer.max_fpstrims render load too.
Troubleshooting
statuscan't reach the daemon (control.sock: no such file) — the wall is running as a different user than your SSH session (checkps -o user= -p "$(pgrep -x sway)"vsid). Re-runinstall.sh <your-user>and reboot so autologin/sway/daemon all run as you.- Duplicate windows / one stream floating loose — two sway sessions are
running (from a getty restart).
sudo rebootfor one clean session. - Blank/black tile — verify the stream directly:
mpv --rtsp-transport=tcp 'rtsps://HOST:7441/ALIAS?enableSrtp'. If it fails, RTSP isn't enabled for that camera (discover --enable-rtsp=high,low). - A tile's video is cut off / overflows — should not happen (mpv is told
--keepaspect-window=noand the daemon re-squares tiles every second); if it does, confirmrtsp-streamer versionis a recent build. login failed/403 settings:editon enable — use a local Protect account; enabling RTSP needs an admin account (view-only 403s).swaymsg not found/ windows not placed — the daemon must run inside the sway session; checkecho $SWAYSOCK.- Build error
package log/slog is not in GOROOT— you're on Debian's Go 1.19; install Go 1.23 into~/.local/goand prepend it toPATH(see Build).
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 incameras: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.
Roadmap
- Import views from the UniFi Protect UI — read Protect's pre-made multi-camera views/layouts and copy them into rtsp-streamer layouts, so you don't rebuild an arrangement you already made in Protect.
- Layout rotation / cycling on a timer (the daemon already re-tiles on demand).
- TUI create / delete / rename layouts (today the TUI only edits existing ones; new layouts are added in YAML).
rtsp-streamer reloadCLI (so hand-edits apply live without the TUI or a restart); optional git tags soversionshows a release rather than a hash.
Layout
cmd/rtsp-streamer/ CLI (cobra): daemon, discover, layout, status, tui, config, version
internal/config/ YAML load/save/validate, schema (tiles, streams), XDG paths
internal/protect/ UniFi Protect client (login, bootstrap, enable RTSP, URLs)
internal/player/ one supervised mpv per stream, JSON IPC, restart/backoff
internal/compositor/ sway control + tile geometry
internal/daemon/ orchestrator + control socket + health loop
internal/ipc/ control-socket protocol shared by daemon and CLI/TUI
internal/tui/ Bubble Tea configurator (grid editor, mouse, quality)
deploy/ sway kiosk config, autologin, install.sh