// NextNVR — MIT License // Copyright (c) 2026 NextNVR Contributors // SPDX-License-Identifier: MIT // Launches and monitors go2rtc for WebRTC stream conversion. package main import ( "fmt" "log" "os" "os/exec" "strings" "sync" "time" ) // Go2RTCManager handles the go2rtc child process lifecycle. type Go2RTCManager struct { mu sync.Mutex cmd *exec.Cmd config Go2RTCConfig running bool } // NewGo2RTCManager creates a new go2rtc process manager. func NewGo2RTCManager(cfg Go2RTCConfig) *Go2RTCManager { return &Go2RTCManager{ config: cfg, } } // Start launches the go2rtc process and generates its configuration. func (g *Go2RTCManager) Start(cameras []CameraConfig) error { g.mu.Lock() defer g.mu.Unlock() if !g.config.Enabled { log.Println("go2rtc: disabled in config, skipping") return nil } // Generate go2rtc.yaml config from camera list. if err := g.writeConfig(cameras); err != nil { return fmt.Errorf("go2rtc config: %w", err) } // Launch go2rtc as a child process. g.cmd = exec.Command(g.config.Binary, "-config", "go2rtc.yaml") g.cmd.Stdout = os.Stdout g.cmd.Stderr = os.Stderr if err := g.cmd.Start(); err != nil { return fmt.Errorf("starting go2rtc: %w", err) } g.running = true log.Printf("go2rtc: started on %s (PID %d)", g.config.Port, g.cmd.Process.Pid) // Monitor the process in the background. go g.monitor() return nil } // Stop terminates the go2rtc process. func (g *Go2RTCManager) Stop() { g.mu.Lock() defer g.mu.Unlock() if g.cmd != nil && g.cmd.Process != nil { log.Println("go2rtc: stopping...") g.cmd.Process.Signal(os.Interrupt) g.mu.Unlock() time.Sleep(2 * time.Second) g.mu.Lock() g.cmd.Process.Kill() g.running = false log.Println("go2rtc: stopped") } } // monitor watches the go2rtc process and restarts it on crash. func (g *Go2RTCManager) monitor() { for { if g.cmd == nil { return } err := g.cmd.Wait() g.mu.Lock() wasRunning := g.running g.mu.Unlock() if !wasRunning { return // intentional shutdown } log.Printf("go2rtc: process exited (%v) — restarting in 5s", err) time.Sleep(5 * time.Second) g.mu.Lock() if g.running { g.cmd = exec.Command(g.config.Binary, "-config", "go2rtc.yaml") g.cmd.Stdout = os.Stdout g.cmd.Stderr = os.Stderr if startErr := g.cmd.Start(); startErr != nil { log.Printf("go2rtc: restart failed: %v", startErr) g.running = false } else { log.Printf("go2rtc: restarted (PID %d)", g.cmd.Process.Pid) } } g.mu.Unlock() } } // writeConfig generates the go2rtc.yaml configuration from camera definitions. func (g *Go2RTCManager) writeConfig(cameras []CameraConfig) error { var sb strings.Builder sb.WriteString("# go2rtc configuration — generated by NextNVR\n") sb.WriteString("api:\n") sb.WriteString(" listen: \"" + g.config.Port + "\"\n\n") sb.WriteString("streams:\n") for _, cam := range cameras { if !cam.Enabled { continue } // Use sub stream for live grid, main stream for full quality. subURL := cam.RTSPSub if subURL == "" { subURL = fmt.Sprintf("rtsp://%s:%s@%s:554/Streaming/Channels/102", cam.Username, cam.Password, cam.IP) } mainURL := cam.RTSPMain if mainURL == "" { mainURL = fmt.Sprintf("rtsp://%s:%s@%s:554/Streaming/Channels/101", cam.Username, cam.Password, cam.IP) } sb.WriteString(fmt.Sprintf(" %s_sub: %s\n", cam.ID, subURL)) sb.WriteString(fmt.Sprintf(" %s_main: %s\n", cam.ID, mainURL)) } return os.WriteFile("go2rtc.yaml", []byte(sb.String()), 0600) }