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>
82 lines
2.0 KiB
Go
82 lines
2.0 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"
|
|
"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 {
|
|
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
|
|
}
|