Files
RTSP-Streamer/README.md
Levi Woodard 8d95c944bd Per-tile stream quality (high/low), fps cap, latency flags
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>
2026-07-02 07:18:57 -05:00

9.4 KiB
Raw Blame History

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 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:

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, 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

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)

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.:

# ~/.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:

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