package main import ( "fmt" "os" "os/exec" "sync" ) // videoController owns the optional fpv-video launcher. It never touches WFB // directly; fpv-video remains the sole owner of the video receiver lifecycle. type videoController struct { command string mu sync.Mutex cmd *exec.Cmd done chan error } func newVideoController(command string) *videoController { return &videoController{command: command} } func (v *videoController) Set(active bool) error { v.mu.Lock() defer v.mu.Unlock() if active { if v.cmd != nil { return nil } v.cmd = exec.Command(v.command) v.cmd.Stdout, v.cmd.Stderr = os.Stdout, os.Stderr if err := v.cmd.Start(); err != nil { v.cmd = nil return fmt.Errorf("start %s: %w", v.command, err) } v.done = make(chan error, 1) cmd := v.cmd go func() { v.done <- cmd.Wait() }() return nil } if v.cmd == nil { return nil } if err := v.cmd.Process.Signal(os.Interrupt); err != nil { return fmt.Errorf("stop %s: %w", v.command, err) } <-v.done v.cmd, v.done = nil, nil return nil } // Running reports whether the video launcher is currently alive. It is the // supervisor's decoded-video presence for the OSD snapshot: the launcher is // the only video decoder this process knows about. A launcher that exited // on its own stays recorded until the next Set; its pending wait result // proves it is no longer alive without consuming the signal the stop path // relies on. Reading this never signals or waits on the launcher, so it // never disturbs the video path. func (v *videoController) Running() bool { v.mu.Lock() defer v.mu.Unlock() if v.cmd == nil { return false } return len(v.done) == 0 } func (v *videoController) Stop() error { return v.Set(false) }