NextNVR/main.go

157 lines
3.7 KiB
Go

// 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 (blocks).
errCh := make(chan error, 1)
go func() {
errCh <- app.StartServer()
}()
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()
}
// 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()
}
}