Files
groundstation/cmd/groundstation/video.go
T

73 lines
1.7 KiB
Go

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)
}