package process import ( "fmt" "log" "os/exec" "sync" "time" "tuner/models" ) // MediaMTXManager manages the MediaMTX child process. type MediaMTXManager struct { mu sync.Mutex cmd *exec.Cmd binaryPath string configPath string startTime time.Time lastError string stopCh chan struct{} stopped bool } // NewMediaMTXManager creates a new manager for the MediaMTX process. func NewMediaMTXManager(binaryPath, configPath string) *MediaMTXManager { return &MediaMTXManager{ binaryPath: binaryPath, configPath: configPath, } } // Start launches the MediaMTX process. It auto-restarts on unexpected exit. func (m *MediaMTXManager) Start() error { m.mu.Lock() defer m.mu.Unlock() if m.cmd != nil && m.cmd.Process != nil { return fmt.Errorf("mediamtx already running (pid %d)", m.cmd.Process.Pid) } return m.startLocked() } func (m *MediaMTXManager) startLocked() error { _, err := exec.LookPath(m.binaryPath) if err != nil { m.lastError = fmt.Sprintf("binary not found: %s", m.binaryPath) return fmt.Errorf("mediamtx binary not found: %s", m.binaryPath) } m.cmd = exec.Command(m.binaryPath, m.configPath) m.lastError = "" m.stopped = false m.stopCh = make(chan struct{}) if err := m.cmd.Start(); err != nil { m.lastError = err.Error() return fmt.Errorf("start mediamtx: %w", err) } m.startTime = time.Now() log.Printf("[mediamtx] started with pid %d", m.cmd.Process.Pid) // Monitor in background and auto-restart on unexpected exit go m.monitor() return nil } func (m *MediaMTXManager) monitor() { err := m.cmd.Wait() m.mu.Lock() defer m.mu.Unlock() // Check if this was a deliberate stop select { case <-m.stopCh: log.Println("[mediamtx] stopped") return default: } if err != nil { m.lastError = err.Error() log.Printf("[mediamtx] exited unexpectedly: %v — restarting in 2s", err) } else { log.Println("[mediamtx] exited unexpectedly (status 0) — restarting in 2s") } m.cmd = nil // Auto-restart after a brief delay (without lock held) go func() { time.Sleep(2 * time.Second) m.mu.Lock() defer m.mu.Unlock() if m.stopped { return } if m.cmd != nil { return // already restarted } if err := m.startLocked(); err != nil { log.Printf("[mediamtx] auto-restart failed: %v", err) } }() } // Stop kills the MediaMTX process. func (m *MediaMTXManager) Stop() error { m.mu.Lock() defer m.mu.Unlock() m.stopped = true if m.cmd == nil || m.cmd.Process == nil { return nil } if m.stopCh != nil { close(m.stopCh) } if err := m.cmd.Process.Kill(); err != nil { return fmt.Errorf("kill mediamtx: %w", err) } m.cmd = nil log.Println("[mediamtx] killed") return nil } // Restart stops and restarts the MediaMTX process. func (m *MediaMTXManager) Restart() error { if err := m.Stop(); err != nil { return err } time.Sleep(500 * time.Millisecond) return m.Start() } // Status returns the current process status. func (m *MediaMTXManager) Status() models.ProcessInfo { m.mu.Lock() defer m.mu.Unlock() info := models.ProcessInfo{ Error: m.lastError, } if m.cmd != nil && m.cmd.Process != nil { info.Running = true info.PID = m.cmd.Process.Pid info.Uptime = time.Since(m.startTime).Truncate(time.Second).String() } return info }