Files
RTSP-Streamer/cmd/rtsp-streamer/main.go
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

70 lines
1.5 KiB
Go

// Command rtsp-streamer is a full-screen RTSP video wall for UniFi Protect
// cameras, driven by mpv under a kiosk Wayland compositor and configured from
// the command line or an interactive TUI.
package main
import (
"fmt"
"log/slog"
"os"
"github.com/lwoodard/rtsp-streamer/internal/config"
"github.com/spf13/cobra"
)
var (
cfgPath string
logLevel string
)
// Build metadata, injected via -ldflags -X at build time (see Makefile).
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
root := &cobra.Command{
Use: "rtsp-streamer",
Short: "Full-screen RTSP camera wall for UniFi Protect",
Version: fmt.Sprintf("%s (commit %s, built %s)", version, commit, date),
SilenceUsage: true,
SilenceErrors: true,
}
root.PersistentFlags().StringVar(&cfgPath, "config", config.DefaultPath(), "path to config file")
root.PersistentFlags().StringVar(&logLevel, "log", "info", "log level: debug|info|warn|error")
root.AddCommand(
daemonCmd(),
discoverCmd(),
layoutCmd(),
reloadCmd(),
statusCmd(),
tuiCmd(),
configCmd(),
versionCmd(),
viewsCmd(),
)
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func newLogger() *slog.Logger {
var lvl slog.Level
switch logLevel {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
}