Fix invisible/flashing clock, map Protect's 8-camera preset, warn on gaps

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
This commit is contained in:
Levi Woodard
2026-07-29 12:17:20 -06:00
parent caed091dc2
commit 81193f524c
10 changed files with 562 additions and 80 deletions

View File

@@ -157,24 +157,53 @@ saving an unrelated edit never blanks the screen.
## Clock overlay
An optional always-on-top clock can be rendered in a screen corner:
An optional always-on-top clock can be rendered at a screen edge:
```yaml
clock:
enabled: true
timezone: America/Denver # IANA name; "Local" uses the system zone. DST auto.
format: "15:04:05" # Go time layout (24-hour w/ seconds)
corner: bottom-right # bottom-right | bottom-left | top-right | top-left
corner: bottom-center # bottom-right | bottom-left | top-right | top-left
# | bottom-center | top-center
background_opacity: 0.45 # alpha of the backing box; must be > 0 (see below)
```
It's a tiny transparent mpv window (no extra dependencies) drawing the time as
**white text with a black outline**, so it stays readable over both bright (day)
and dark (night) camera scenes without measuring the picture. The time is
formatted in the configured timezone via Go's zone database, so it's correct
regardless of the host clock's zone and handles DST on its own. The daemon keeps
it positioned and on top across layout switches. Size (`width`/`height`),
`font_size`, and edge `margin` are configurable; enable/disable or retune it
live with an edit plus `rtsp-streamer reload`.
It's a tiny mpv window (no extra dependencies) drawing the time as **white text
with a black outline** over a dimmed backing box, so it stays readable over both
bright (day) and dark (night) camera scenes without measuring the picture. The
time is formatted in the configured timezone via Go's zone database, so it's
correct regardless of the host clock's zone and handles DST on its own. The
daemon keeps it positioned and on top across layout switches. Size
(`width`/`height`), `font_size`, and edge `margin` are configurable;
enable/disable or retune it live with an edit plus `rtsp-streamer reload`.
The `*-center` positions center the window horizontally and ignore `margin` on
that axis; `margin` still applies vertically.
### Why the clock is drawn the way it is
The time is baked into every video frame by an ffmpeg `drawtext` filter that
re-reads a small text file (`$XDG_RUNTIME_DIR/rtsp-streamer/clock-text.txt`),
which the daemon rewrites once a second. That looks roundabout — pushing an ASS
`osd-overlay` over mpv's IPC would be the obvious approach — but on mpv 0.35 +
Mesa/V3D + sway an OSD overlay is rendered **only on the frame where its content
changes**, so an IPC-driven clock is visible for roughly 200ms per second and
reads as a flashing clock. Every layer reports success while this happens
(`error: success` from mpv, `vo-configured: true`, sway reporting the window
visible at the right rect), so it looks like a stacking or font bug and is
neither. Two constraints follow, and both are covered by tests:
- **`background_opacity` must be greater than zero.** A fully transparent canvas
(`color=c=black@0.0` with `--alpha=yes`) makes the drawn glyphs inherit alpha
0, and the compositor shows nothing at all. Values of `0` or below — including
the field being absent — are clamped to the default.
- **The canvas must not be buffered ahead** (`--cache=no`,
`--demuxer-readahead-secs=0`). `drawtext` stamps the time when a frame is
*generated*, so reading ahead makes the displayed clock lag by the readahead
and swallows text-file updates entirely.
Because the text arrives through a file, the clock window needs no IPC socket.
## UniFi Protect live views
@@ -187,10 +216,29 @@ rtsp-streamer views import --all # import every view
rtsp-streamer views dump # raw view JSON (for tuning odd layouts)
```
Cameras are matched by Protect id, so run `discover` first. Grid size is
inferred from the slot count (asymmetric "1 big + N" presets land as an even
grid for now — send me `views dump` output to map exact sizing). An imported
layout is **linked** to its view (`protect_view:` in the config).
Cameras are matched by Protect id, so run `discover` first. An imported layout is
**linked** to its view (`protect_view:` in the config).
Protect's arrangement for a view is not always an even grid. Known asymmetric
presets are mapped exactly; any other slot count falls back to a near-square grid
of equal tiles:
| Slots | Result |
| --- | --- |
| 8 | `5x4` — four 2x2 tiles plus a right-hand column of four 1x1 tiles |
| anything else | near-square uniform grid (`GridForSlots`) |
To add another preset, run `views dump`, note the view's `layout` value and slot
order, and add an entry to `presets` in `internal/viewmap/viewmap.go`. Slot order
in the dump is the order tiles are filled.
Import **warns** whenever a mapping would leave grid cells empty (they show as
blank rectangles on the wall) or references a camera that isn't in the config:
```
! Office View: 1 of 9 cells in the 3x3 grid are empty and will show as blank areas
! Office View: slot 4 camera 6a65…c6d not in config (run `discover`)
```
### Auto-resync
@@ -321,6 +369,26 @@ pegged, work through:
- **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 stays blank but its mpv process is running** — the camera entry is
stale. Re-adopting a camera in Protect gives it a **new id and often a slightly
different name** (`Livingroom` → `Living Room`), and `discover` only adds and
updates, it never prunes — so the old entry lingers with an alias the controller
no longer serves. Process counts look healthy because mpv is up and retrying.
Diff your config's aliases against the controller:
`curl -sk -b cookies.txt https://HOST/proxy/protect/api/bootstrap` and compare
each camera's `channels[].rtspAlias`. Delete the dead entries, then fix every
layout that referenced them (watch for a camera ending up twice after a rename).
- **Cameras in `cameras:` aren't on screen** — only cameras placed as tiles in the
**active layout** are streamed, one mpv per tile. The `cameras:` list is just an
inventory. Check with `rtsp-streamer layout ls` (it prints each layout's tile
count).
- **Black bars inside every tile** — the base grid's cells aren't 16:9, so mpv
letterboxes each feed to fit. On a 16:9 output only a *square* grid (`NxN`)
yields 16:9 cells; e.g. a `5x4` grid on 3840x2160 gives 768x540 (1.42:1) cells.
Some arrangements can't avoid this at all — two 16:9 tiles 1080px tall need the
full 3840px width, leaving no room for a side column. To fill instead of pad,
crop with `player.extra_args: ["--panscan=1.0"]`, at the cost of roughly the top
and bottom 20% of each camera's field of view.
- **A tile's video is cut off / overflows** — should not happen (mpv is told
`--keepaspect-window=no` and the daemon re-squares tiles every second); if it
does, confirm `rtsp-streamer version` is a recent build.