The TUI is now a TypeScript/opentui app in tui/ rather than Bubble Tea.
opentui is a Zig core with TypeScript bindings and no Go bindings, so this half
of the tool can't live in the Go binary; it compiles with Bun into a sibling
executable (rtsp-streamer-tui) that `rtsp-streamer tui` execs.
Everything that isn't presentation stays in Go, reached over three JSON
commands. The configurator holds no credentials and never writes the config
itself:
config export the config, plus limits like max_tiles
config apply (stdin) merge cameras/layouts/active_layout, validate, save
atomically, reload the daemon
discover --json Protect discovery, writing nothing
Two properties of that split are deliberate:
- The controller password never crosses the bridge. It's json:"-" on the way
out, and apply only merges the three keys the TUI edits, so it can't be
clobbered on the way back in either.
- apply re-reads the file before merging, so an editor left open for an hour can
no longer overwrite a `views import`, a `layout set`, or a hand edit made in
the meantime.
Discovery is previewable as a result: `discover --json` writes nothing, the
merge happens in the TUI, and nothing reaches disk until you save. Only
--enable-rtsp has a side effect, and it's on the controller.
Config structs gain json tags mirroring their yaml ones so the config
round-trips through the bridge under the same key names it has on disk, and
maxGridDim moves to config.MaxGridDim so the CLI and both configurators enforce
one ceiling. The write path is byte-for-byte identical to `layout set`, checked
against a copy of a live config.
Visible change: the grid editor draws real bordered boxes, so a spanning tile is
one box instead of an origin cell plus "·" continuation marks, and the
header-offset arithmetic in mouse.go is gone — the framework hit-tests list
rows. Keybindings, the lipgloss palette and the screen flow are carried over
unchanged; S now saves from anywhere.
The Bubble Tea version stays as `tui --legacy`. It's compiled into the Go binary
and needs no Bun, and on a headless Pi the TUI is the only config UI there is,
so a fallback is worth its weight. The cost of the new one is size: ~120 MB
against ~13 MB, since Bun embeds its runtime and opentui's native library.
Tests: 67 bun tests drive the real (in-memory) opentui renderer, including mouse
click and drag, plus tsc --noEmit. `make test-tui` runs both, and
scripts/preview.ts dumps every screen as text without needing a terminal.
Three bugs found during the port are documented in tui/README.md, since none are
apparent from the code: overlapping cell borders render as ┌ where a lattice
needs ┬; a drag dies after the first resize if the tree is rebuilt, because the
renderer captures the press-target renderable; and a rebuilt box has no computed
layout until the next frame, so its screenX reads 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
118 lines
5.5 KiB
YAML
118 lines
5.5 KiB
YAML
# Example rtsp-streamer config. Copy to ~/.config/rtsp-streamer/config.yaml
|
|
# (or point $RTSP_STREAMER_CONFIG at it) and edit. Keep secrets out of it by
|
|
# using password_env instead of an inline password.
|
|
|
|
controller:
|
|
host: 192.168.1.1 # UniFi OS console IP / hostname
|
|
username: viewer # a local Protect user with camera view access
|
|
password_env: RTSP_STREAMER_PASSWORD # read the password from this env var
|
|
verify_tls: false # UniFi ships a self-signed cert
|
|
rtsp_port: 7441 # Protect RTSPS port (default)
|
|
|
|
# Pin the render resolution, or leave empty to auto-detect from the connected
|
|
# output via sway.
|
|
display:
|
|
width: 1920
|
|
height: 1080
|
|
|
|
player:
|
|
hwdec: v4l2m2m-copy # Pi 4 hardware H.264 decoder. "auto-safe" often
|
|
# won't engage it; "no" forces software.
|
|
profile: low-latency
|
|
max_fps: 0 # cap rendered fps (0 = uncapped); trims render load
|
|
audio: false # streams are muted by default (saves CPU too);
|
|
# set true to decode and play camera audio
|
|
resync_seconds: 600 # reconnect each stream to the live edge on this
|
|
# interval (staggered) so latency can't slowly
|
|
# drift; 0 = off. 600 keeps drift to a few sec.
|
|
restart_backoff_seconds: 3
|
|
extra_args: [] # appended verbatim to every stream's mpv.
|
|
# ["--panscan=1.0"] crops each feed to fill its
|
|
# tile instead of letterboxing it — useful when
|
|
# the grid's cells aren't 16:9, at the cost of the
|
|
# top/bottom of the field of view.
|
|
|
|
# Camera catalog. Normally populated by `rtsp-streamer discover`; shown here
|
|
# by hand for illustration. Each camera can carry multiple stream qualities
|
|
# (high/medium/low) so a tile can choose which to pull. Layouts reference
|
|
# cameras by `name`. (A single `rtsp:` URL instead of `streams:` also works.)
|
|
cameras:
|
|
- id: 60a1b2c3d4e5f6 # UniFi Protect camera id (blank for manual entries)
|
|
name: Front Door
|
|
streams:
|
|
high: rtsps://192.168.1.1:7441/aBcD1234?enableSrtp
|
|
low: rtsps://192.168.1.1:7441/aBcD5678?enableSrtp
|
|
- name: Driveway
|
|
streams:
|
|
high: rtsps://192.168.1.1:7441/eFgH1234?enableSrtp
|
|
low: rtsps://192.168.1.1:7441/eFgH5678?enableSrtp
|
|
- name: Back Yard
|
|
streams:
|
|
low: rtsps://192.168.1.1:7441/iJkL9012?enableSrtp
|
|
- name: Garage
|
|
rtsp: rtsps://192.168.1.1:7441/mNoP3456?enableSrtp # legacy single-URL form
|
|
|
|
# Layouts place cameras on a base grid (COLSxROWS). Cameras are "tiles" that
|
|
# may span multiple cells, so you can mix a big main view with small side
|
|
# tiles. Up to 16 cameras per layout. Edit these interactively with
|
|
# `rtsp-streamer tui` (a live preview shows the arrangement as you edit).
|
|
layouts:
|
|
# Simple even grid: four 1x1 tiles on a 2x2.
|
|
- name: quad
|
|
grid: 2x2
|
|
tiles:
|
|
- {camera: Front Door, col: 0, row: 0}
|
|
- {camera: Driveway, col: 1, row: 0}
|
|
- {camera: Back Yard, col: 0, row: 1}
|
|
- {camera: Garage, col: 1, row: 1}
|
|
|
|
# Security-wall style: one big 3x3 main view (high quality) + a right column
|
|
# of three small tiles on the low substream to keep decode load down.
|
|
- name: main-plus
|
|
grid: 4x3
|
|
tiles:
|
|
- {camera: Front Door, col: 0, row: 0, colspan: 3, rowspan: 3, quality: high}
|
|
- {camera: Driveway, col: 3, row: 0, quality: low}
|
|
- {camera: Back Yard, col: 3, row: 1, quality: low}
|
|
- {camera: Garage, col: 3, row: 2, quality: low}
|
|
|
|
- name: front-focus
|
|
grid: 1x1
|
|
tiles:
|
|
- {camera: Front Door, col: 0, row: 0}
|
|
|
|
# The older one-camera-per-cell "slots" form still works and is upgraded to
|
|
# tiles automatically when you edit it:
|
|
# grid: 3x3
|
|
# slots: [Front Door, Driveway, Back Yard, Garage, "", "", "", "", ""]
|
|
|
|
active_layout: quad
|
|
|
|
# On-screen clock overlay: a small always-on-top window showing the local time,
|
|
# drawn as white text with a black outline so it stays legible over both bright
|
|
# (day) and dark (night) camera scenes. The time is computed in the given
|
|
# timezone (DST handled automatically), independent of the host clock's zone.
|
|
clock:
|
|
enabled: true
|
|
timezone: America/Denver # IANA name; "Local" uses the system zone
|
|
format: "15:04:05" # Go time layout (24-hour w/ seconds). Others:
|
|
# "3:04:05 PM" · "Mon Jan 2 15:04"
|
|
corner: bottom-right # bottom-right | bottom-left | top-right | top-left
|
|
# | bottom-center | top-center
|
|
# the *-center positions center horizontally and
|
|
# ignore margin on that axis
|
|
font_size: 44 # glyph height in px
|
|
width: 300 # overlay window size in px
|
|
height: 72
|
|
margin: 24 # gap from the screen edges in px
|
|
background_opacity: 0.45 # alpha of the backing box behind the text.
|
|
# MUST be > 0: on a fully transparent canvas the
|
|
# glyphs inherit alpha 0 and nothing is drawn at
|
|
# all. 0/absent is clamped to this default.
|
|
|
|
# Re-sync the active layout from its linked UniFi Protect live view every N
|
|
# seconds (0 = off). Layouts created by `views import` carry a `protect_view:`
|
|
# and are re-synced when this is set. Requires the controller password at
|
|
# runtime (export RTSP_STREAMER_PASSWORD for the daemon).
|
|
view_refresh_seconds: 0
|