Commit Graph

16 Commits

Author SHA1 Message Date
Levi Woodard
caed091dc2 Add on-screen clock overlay (outlined text, configurable corner/timezone)
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>
2026-07-02 14:20:49 -05:00
Levi Woodard
840324825b Bound live-stream latency drift with periodic live-edge resync
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>
2026-07-02 14:09:07 -05:00
Levi Woodard
ed9814ab4e Reconnect streams in-process; fix for_window IPC rules
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>
2026-07-02 12:54:02 -05:00
Levi Woodard
be1b53ae04 Restart without reboot; retrievable logs incl. mpv exit reasons
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>
2026-07-02 10:03:25 -05:00
Levi Woodard
9985a93851 Place windows at map time so stream restarts never flash
Diagnosis: individual tiles flickered when their stream died and mpv
relaunched (confirmed via status: a PID dropping to 0 and coming back).
The restarted window mapped at the video's native size wherever sway
dropped it, and the corrective loop only snapped it into its cell on the
next tick — up to 2s later on the relaxed cadence.

Fix, at the source instead of racing the map:
- Pre-install a per-slot for_window rule (matched on each mpv's unique
  window title, anchored so slot-1 never matches slot-10) so sway
  positions and sizes the window synchronously the moment it maps.
- Pass --geometry=WxH per tile so mpv opens at the tile size rather
  than resizing itself to the video's native size on load.

The placeLoop remains as a corrective backstop for mid-life drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:50:39 -05:00
Levi Woodard
98ddf1645c Smoother daemon: no-op reloads, batched placement, muted audio, graceful teardown
- 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>
2026-07-02 07:38:01 -05:00
Levi Woodard
291d37ec83 Import UniFi Protect live views + optional auto-resync
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>
2026-07-02 07:18:57 -05:00
Levi Woodard
9da5ace150 docs: update README for tiles, mouse, per-tile quality, deploy, roadmap
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>
2026-07-02 07:18:57 -05:00
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
Levi Woodard
780396076d Add mouse support to the TUI (tmux-style tile resize)
Enable mouse in the Bubble Tea program. In the layout grid editor:
click a cell to assign a camera, or press on a tile and drag to
resize its span (grid-snapped). Click-to-select on the menu/list
screens and the camera picker. Works over SSH; edits the config,
so it never fights the daemon geometry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:07:18 -05:00
Levi Woodard
9300266f5c Fit any-aspect camera into its tile without cropping
Add --keepaspect-window=no so mpv stops reshaping its window to the
video aspect (which pushed a 4:3 camera below its tile and hid the
bottom behind the neighbour). Combined with keepaspect=yes the video
is letterboxed to fit the tile for any camera type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:54:33 -05:00
Levi Woodard
477246d574 Add --version / version with baked-in git commit
ldflags inject version, commit, and build date; a version subcommand
also reports go version and platform. Ends the which-binary-is-this
guessing after deploys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:47:44 -05:00
Levi Woodard
eb05c48db8 Harden health probe against false restarts
Probe mpv-version (always present) instead of time-pos, which live
streams with caching off can report unavailable while playing fine —
that produced false unhealthy verdicts. Also require 3 consecutive
misses (~60s) before force-restarting a hung stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:41:48 -05:00
Levi Woodard
0fb731a71e Fix mpv tile overlap portably; add make install/deploy
mpv 0.35 rejects --auto-window-resize, which crash-looped every player.
Drop that flag and re-assert each tile geometry on a 1s timer in the
daemon, overriding mpv auto-resize on any mpv version.

Add make install (auto-sudo, native build + copy to /usr/local/bin)
and make deploy (install + restart the kiosk getty unit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:36:36 -05:00
Levi Woodard
ca74ba0070 adding resize rules 2026-07-01 19:28:58 -05:00
Levi Woodard
55a8ea4bee adding to git.i0t.app 2026-07-01 18:21:14 -05:00