// NextNVR — MIT License // Copyright (c) 2026 NextNVR Contributors // SPDX-License-Identifier: MIT // Lightweight NVR application for recording and monitoring IP cameras. package main import ( "fmt" "log" "os" "os/signal" "syscall" ) var ( // Version is set at build time via ldflags. Version = "0.1.0" // BuildTime is set at build time via ldflags. BuildTime = "unknown" ) func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.Printf("NextNVR v%s (built %s)", Version, BuildTime) // Parse command-line flags. configPath := flagConfigPath() // Load or create configuration. cfg, err := LoadConfig(configPath) if err != nil { log.Printf("No existing config found at %s — starting in setup mode", configPath) cfg = DefaultConfig() // Don't auto-save; wait for the user to complete setup via the web UI. } log.Printf("Server: %s", cfg.Server.Port) log.Printf("Storage: %s (retention: %d days)", cfg.Storage.RecordingsPath, cfg.Storage.RetentionDays) log.Printf("Cameras configured: %d", len(cfg.Cameras)) if cfg.Go2RTC.Enabled { log.Printf("go2rtc: enabled on port %s (binary: %s)", cfg.Go2RTC.Port, cfg.Go2RTC.Binary) } // Signal handling for graceful shutdown. sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) // Initialize application. app := &App{Config: cfg, ConfigPath: configPath} // Start go2rtc if enabled. if cfg.Go2RTC.Enabled { go2rtc := NewGo2RTCManager(cfg.Go2RTC) if err := go2rtc.Start(cfg.Cameras); err != nil { log.Printf("go2rtc: start failed: %v", err) } app.go2rtc = go2rtc } // Start snapshot engine for all enabled cameras. snaps := NewSnapshotEngine(cfg.Storage) snaps.StartAll(cfg.Cameras) app.snapshots = snaps // Start FFmpeg recorder for all enabled cameras. rec := NewRecorderManager(cfg.Storage) rec.StartAll(cfg.Cameras) app.recorder = rec // Start retention cleaner. cln := NewCleaner(cfg.Storage) go cln.Start() app.cleaner = cln // Start the HTTP server (main port, with auth). app.server, _ = NewServer(cfg) app.server.onReload = func() { log.Println("Config reload — restarting services...") if app.go2rtc != nil { app.go2rtc.Stop() } if app.snapshots != nil { app.snapshots.StopAll() } if app.recorder != nil { app.recorder.StopAll() } // Restart with the updated config from the server's in-memory copy. liveCfg := app.server.appConfig if liveCfg.Go2RTC.Enabled { app.go2rtc = NewGo2RTCManager(liveCfg.Go2RTC) app.go2rtc.Start(liveCfg.Cameras) } snaps := NewSnapshotEngine(liveCfg.Storage) snaps.StartAll(liveCfg.Cameras) app.snapshots = snaps rec := NewRecorderManager(liveCfg.Storage) rec.StartAll(liveCfg.Cameras) app.recorder = rec log.Println("Config reload — services restarted.") } errCh := make(chan error, 1) go func() { errCh <- app.server.ListenAndServe() }() // Start viewer server (no auth, live wall only) — after main server is created. if cfg.Server.ViewerPort != "" { go func() { if err := app.server.StartViewerServer(); err != nil { log.Printf("Viewer server: %v", err) } }() } select { case sig := <-sigCh: log.Printf("Received signal: %v — shutting down", sig) case err := <-errCh: if err != nil { log.Printf("Server error: %v", err) } } app.Shutdown() log.Println("NextNVR stopped.") } // flagConfigPath resolves the config file path from CLI flags. func flagConfigPath() string { // Look for --config flag in os.Args. for i, arg := range os.Args[1:] { if arg == "--config" && i+1 < len(os.Args[1:]) { return os.Args[i+2] // +2 because Args[0] is binary name, Args[1:] starts at index 1 } if len(arg) > 9 && arg[:9] == "--config=" { return arg[9:] } } // Default: look next to the binary, then /opt/nextnvr. if _, err := os.Stat("config.yaml"); err == nil { return "config.yaml" } if _, err := os.Stat("/opt/nextnvr/config.yaml"); err == nil { return "/opt/nextnvr/config.yaml" } return "config.yaml" } // App holds the application runtime state. type App struct { Config Config ConfigPath string server *Server go2rtc *Go2RTCManager snapshots *SnapshotEngine recorder *RecorderManager cleaner *Cleaner } // StartServer initializes and starts the HTTP server. func (a *App) StartServer() error { srv, err := NewServer(a.Config) if err != nil { return fmt.Errorf("creating server: %w", err) } a.server = srv return srv.ListenAndServe() } // StartViewerServer launches the viewer-only server. func (a *App) StartViewerServer() error { if a.server == nil { return fmt.Errorf("main server not initialized") } return a.server.StartViewerServer() } // Shutdown performs a graceful shutdown of all services. func (a *App) Shutdown() { if a.recorder != nil { a.recorder.StopAll() } if a.cleaner != nil { a.cleaner.Stop() } if a.snapshots != nil { a.snapshots.StopAll() } if a.go2rtc != nil { a.go2rtc.Stop() } if a.server != nil { a.server.Close() } }