diff --git a/fileplayback/player.go b/fileplayback/player.go index bb8736e..4c658b9 100644 --- a/fileplayback/player.go +++ b/fileplayback/player.go @@ -21,6 +21,7 @@ type Player struct { stopChan chan struct{} ctx context.Context cancel context.CancelFunc + cmd *exec.Cmd mutex sync.Mutex wg sync.WaitGroup playing bool @@ -99,6 +100,7 @@ func (p *Player) Stop() error { if p.cancel != nil { p.cancel() } + terminateProcessGroup(p.cmd) } p.mutex.Unlock() @@ -106,7 +108,7 @@ func (p *Player) Stop() error { // worker have exited, otherwise old audio can enter the new playback. p.wg.Wait() p.mutex.Lock() - p.playing, p.stopping, p.cancel = false, false, nil + p.playing, p.stopping, p.cancel, p.cmd = false, false, nil, nil localPlayback := p.localPlayback p.mutex.Unlock() if localPlayback != nil { @@ -159,6 +161,7 @@ func (p *Player) readFileAudio() { ctx := p.ctx p.mutex.Unlock() cmd := exec.CommandContext(ctx, "ffmpeg", args...) + configureProcessGroup(cmd) pipe, err := cmd.StdoutPipe() if err != nil { p.mutex.Lock() @@ -175,6 +178,9 @@ func (p *Player) readFileAudio() { p.reportError(errors.New("failed to start ffmpeg: " + err.Error())) return } + p.mutex.Lock() + p.cmd = cmd + p.mutex.Unlock() // Stereo has 2 channels, so we need twice the buffer size byteBuffer := make([]byte, frameSize*2*2) // frameSize * 2 channels * 2 bytes per sample @@ -185,7 +191,7 @@ func (p *Player) readFileAudio() { for { select { case <-p.stopChan: - cmd.Process.Kill() + terminateProcessGroup(cmd) cmd.Wait() return case <-ticker.C: diff --git a/fileplayback/process_other.go b/fileplayback/process_other.go new file mode 100644 index 0000000..744f15e --- /dev/null +++ b/fileplayback/process_other.go @@ -0,0 +1,13 @@ +//go:build !(aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) + +package fileplayback + +import "os/exec" + +func configureProcessGroup(cmd *exec.Cmd) {} + +func terminateProcessGroup(cmd *exec.Cmd) { + if cmd != nil && cmd.Process != nil { + _ = cmd.Process.Kill() + } +} diff --git a/fileplayback/process_unix.go b/fileplayback/process_unix.go new file mode 100644 index 0000000..00b15bf --- /dev/null +++ b/fileplayback/process_unix.go @@ -0,0 +1,19 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package fileplayback + +import ( + "os/exec" + "syscall" +) + +func configureProcessGroup(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} + +func terminateProcessGroup(cmd *exec.Cmd) { + if cmd == nil || cmd.Process == nil { + return + } + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) +} diff --git a/fileplayback/process_unix_test.go b/fileplayback/process_unix_test.go new file mode 100644 index 0000000..3fed92a --- /dev/null +++ b/fileplayback/process_unix_test.go @@ -0,0 +1,16 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package fileplayback + +import ( + "os/exec" + "testing" +) + +func TestConfigureProcessGroupCreatesSeparateGroup(t *testing.T) { + cmd := exec.Command("true") + configureProcessGroup(cmd) + if cmd.SysProcAttr == nil || !cmd.SysProcAttr.Setpgid { + t.Fatal("ffmpeg process was not configured to lead its own process group") + } +}