148 lines
3.5 KiB
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)
|
|
}
|