NextNVR/go2rtc.go

148 lines
3.5 KiB
Go

// 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", "/opt/nextnvr/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", "/opt/nextnvr/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("/opt/nextnvr/go2rtc.yaml", []byte(sb.String()), 0600)
}