// 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 ( "errors" "fmt" "io" "log/slog" "os" "github.com/lwoodard/rtsp-streamer/internal/config" "github.com/lwoodard/rtsp-streamer/internal/logbuf" "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(), logsCmd(), reloadCmd(), restartCmd(), statusCmd(), tuiCmd(), configCmd(), versionCmd(), viewsCmd(), ) if err := root.Execute(); err != nil { // Bridge commands report their failure as JSON on stdout and return // errQuiet, so we exit non-zero without a second error line. if !errors.Is(err, errQuiet) { fmt.Fprintln(os.Stderr, "error:", err) } os.Exit(1) } } func logLevelValue() slog.Level { switch logLevel { case "debug": return slog.LevelDebug case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // newDaemonLogger builds the daemon's logger, teeing output to stderr and to // an in-memory ring so `rtsp-streamer logs` can retrieve recent activity over // the control socket (the daemon's stderr itself is not reachable on the // headless kiosk — it goes to sway on tty1). func newDaemonLogger() (*slog.Logger, *logbuf.Writer) { ring := logbuf.New(500) h := slog.NewTextHandler(io.MultiWriter(os.Stderr, ring), &slog.HandlerOptions{Level: logLevelValue()}) return slog.New(h), ring }