Three separate faults made cameras "not load" and the clock misbehave.
Clock rendered as nothing, or flashed ~200ms/second. The time was drawn as
an ASS osd-overlay pushed over mpv IPC, but on mpv 0.35 + Mesa/V3D + sway an
OSD overlay is rendered only on the frame where its content *changes*. Every
layer reports success while this happens (mpv returns error:success,
vo-configured is true, and sway reports the window visible at the right
rect), so it looks like a stacking or font bug and is neither. Ruled out:
pushing at 20Hz (identical content is ignored, so it still only redrew when
the second flipped), osd-msg1, show-text, and --pause (mpv stops redrawing
entirely). Fonts were never the issue.
The time is now baked into every frame by a drawtext filter re-reading a
small file the ticker rewrites once a second, with two constraints that cost
real time to find and are pinned by tests:
- The canvas alpha must be > 0. A fully transparent canvas (black@0.0 with
--alpha=yes) makes the glyphs inherit alpha 0 and the compositor draws
nothing -- this was the original invisible clock. New clock
background_opacity (default 0.45) is clamped in config *and* in args() so
no code path can produce an invisible clock.
- Readahead must be off. drawtext stamps the time when a frame is
*generated*, so buffering ahead makes the visible clock lag by the
readahead and swallows text-file updates entirely.
Since the text now arrives through a file, the clock needs no IPC socket:
dropped --input-ipc-server, the ipcPath field, and the stale-socket removal.
assEscape goes with the ASS path.
`views import` produced layouts with holes. viewmap derived the grid from
the slot count alone and ignored Protect's `layout` field, so Protect's
asymmetric 8-camera preset (four 2x2 tiles plus a right column of four 1x1)
landed as 8 tiles in a 3x3 grid -- the bottom-right cell was simply empty
and rendered as a blank rectangle. That preset is now mapped exactly; other
counts keep the uniform GridForSlots fallback rather than guessing at
presets I have not observed. Import also warns when a mapping would leave
empty cells or references a camera missing from the config, so a silent hole
cannot reach the screen again.
Also:
- clock.corner gains bottom-center and top-center (centered horizontally,
Margin still applies vertically).
- placeClock no longer re-issues `resize set` every tick. Re-asserting
geometry on a correctly-sized window makes sway send a configure event,
which makes mpv reallocate buffers and blank for a frame. New
compositor.Raise re-asserts z-order only, which is all the 2s tick needs;
geometry is re-placed only when it has actually drifted.
- README documents why the clock is drawn this way, the preset table and how
to add another from `views dump`, and three troubleshooting entries for
failure modes that all look like bugs: a blank tile whose mpv is running
(a stale camera entry -- re-adopting a camera in Protect assigns a new id
and often a slightly different name, and `discover` never prunes), cameras
in `cameras:` not being on screen (only the active layout's tiles stream),
and black bars inside tiles (non-16:9 grid cells; --panscan=1.0 crops to
fill instead).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYhTnkp7VzJ67THeicgfAQ
A small always-on-top mpv window renders the local time in a screen
corner. Implementation notes:
- Transparent lavfi canvas (color=...@0.0, --alpha=yes) so the camera
video shows through; no new dependencies. If the compositor can't do
alpha it degrades to a dark backing, still legible.
- Time drawn as an ASS osd-overlay pushed over mpv IPC once a second:
white fill + black outline (\bord), so it reads on both bright (day)
and dark (night) scenes without sampling the picture. Formatting the
text in Go avoids any filtergraph escaping.
- Time computed with time.LoadLocation against a configured IANA zone
(default "Local"), so it's correct regardless of the host clock's zone
and handles DST. A bad zone name fails at startup.
- Managed on the daemon's own context (survives layout switches); the
daemon keeps it positioned and raised above camera tiles. ensureClock
is a no-op when the clock config + resolution are unchanged, so a
reload never disturbs it.
Config: new `clock` section (enabled, timezone, format, corner,
font_size, width, height, margin) with defaults and corner validation.
Documented in README and config.example (shipped enabled, America/Denver,
24-hour w/ seconds). Tests cover corner geometry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Over hours the wall drifted ~15s behind real-time: the Pi decodes a hair
slower than real-time, and since RTSP-over-TCP delivers every byte, that
small deficit buffers up instead of being dropped — and a live stream
can't be seeked back to the live edge.
Add player.resync_seconds (0 = off): the daemon reconnects each stream to
the live edge on that interval via mpv loadfile-replace over IPC, cycling
one tile at a time so only a single tile ever blips. Reusing the running
mpv process means no window teardown. At 600s this caps drift to a couple
seconds. config.example.yaml ships it at 600; README documents the knob
and the "also lighten decode load" caveat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the periodic drops (found via the new logs): mpv was
exiting cleanly (err=nil, exit 0) with its status line at 100% — i.e.
end-of-stream. UniFi Protect closes some cameras' RTSP connections every
20-40s; with --keep-open=no --idle=no mpv treated that as the file
ending, exited, and the supervisor relaunched it ~3s later (the visible
drop).
- Add --loop-file=inf so mpv reopens the stream the instant it EOFs,
in the same process and window: sub-second recovery, no teardown,
no supervisor bounce. The supervisor now only fires for real crashes.
- Clean up the logged exit "reason": strip ANSI escapes and skip mpv's
transient A/V status prints, so a genuine error surfaces instead of
"\x1b[KV: 00:00:35 / 00:00:35 (100%)". Tests cover the parsing.
Also fold in the for_window IPC fix: sway's IPC parser splits comma-
joined command lists at the top level and does not fold the continuation
into for_window (unlike the config-file parser), so the map-time
placement rules errored with "Only views can have borders" and never
installed. Register each command separately; drop the redundant
border none (the kiosk config already sets default_border none).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related gaps on the headless kiosk: no way to restart the wall short
of a full reboot, and no way to see why a stream keeps flapping (the
daemon's stderr goes to sway on tty1, and mpv's stderr was discarded).
Restart in place:
- `rtsp-streamer restart` sends a control-socket command; the daemon
tears down its mpv children and syscall.Exec's the on-disk binary.
Same PID, same parent (sway), same env — keeps WAYLAND_DISPLAY/
SWAYSOCK and comes back up on the new binary, no reboot. Handles the
os.Executable() "(deleted)" sentinel from make install's rename.
- sway config now launches the daemon in a relaunch loop, so a crash (or
the restart) auto-recovers instead of leaving a black screen.
- `make deploy` now does `install` + `restart` instead of restarting
getty@tty1 (which left stale duplicate sessions and forced reboots).
Retrievable logs:
- New internal/logbuf ring; the daemon tees slog output into it and
serves the tail over the socket via `rtsp-streamer logs [-n N]` —
readable over SSH, no file wrangling, no reboot.
- Capture the tail of each mpv's stderr and log its last line when the
process exits, so "mpv exited, will restart" now carries the reason
(connection refused, unsupported codec, 401, ...). This is the
diagnostic for a single tile going unhealthy repeatedly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Skip stream restarts when a reload resolves to the identical wall
(same URLs, tile geometry, player settings) — saving an unrelated
config edit no longer blanks the screen.
- Replace per-tile placement polling (a swaymsg fork per tile per second,
plus 150ms get_tree polling per tile at startup) with one shared loop:
a single get_tree snapshot per tick, re-placing only drifted windows,
relaxing to a 2s tick once settled.
- Mute streams by default (--no-audio) to skip an audio decoder per
stream; opt back in with player.audio: true.
- Graceful, parallel mpv teardown via cmd.Cancel/WaitDelay, fixing the
double-Wait race between Stop and Supervise and cutting worst-case
layout switches from ~2s x N streams to ~2s total.
- status: probe mpv IPC outside the daemon mutex so a slow probe can't
block layout switches.
- View sync: reuse the Protect session across ticks (re-login only on
expiry) and pick up view_refresh_seconds changes without a restart.
- Health strikes keyed by player, not slot, so they never carry across
layout switches; prune stale entries.
- New `rtsp-streamer reload` CLI; fix dead sort in `layout ls`; back off
on persistent control-socket accept errors; fsync config before the
atomic rename (SD-card power-cut safety); bump IPC client deadline;
gofmt stragglers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
views ls/dump/import copy a Protect live view (cameras + grid) into a
layout; grid inferred from slot count (asymmetric presets TBD, see
views dump). Imported layouts link back via protect_view.
Daemon re-syncs the active linked view every view_refresh_seconds (0=
off), so Protect-side edits appear on the wall. Needs the controller
password at runtime; off by default keeps the wall credential-free.
Shared viewmap package used by CLI and daemon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>