Make ffmpeg pause nonblocking

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 14:41:35 -04:00
committed by Brandon McGinty
parent e564bb8583
commit 977b690eed
3 changed files with 34 additions and 4 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ Priority 1: transport, lifecycle, and correctness
output file creation fails, the tone goroutine continues. Create the saver
first, or close/wait for the tone generator and reset Tx on every failure.
15. Gumble ffmpeg Pause can block forever
[x] 15. Gumble ffmpeg Pause can block forever
File: gumble/gumbleffmpeg/stream.go
Pause checks StatePlaying, releases the lock, then sends on an unbuffered
pause channel. If process exits in between, no receiver remains. Redesign
+11 -3
View File
@@ -57,7 +57,7 @@ func New(client *gumble.Client, source Source) *Stream {
Volume: 1.0,
Source: source,
Command: "ffmpeg",
pause: make(chan struct{}),
pause: make(chan struct{}, 1),
state: StateInitial,
}
}
@@ -124,7 +124,12 @@ func (s *Stream) Pause() error {
}
s.state = StatePaused
s.l.Unlock()
s.pause <- struct{}{}
// The process can exit after the state check. A buffered, coalesced pause
// request preserves the state transition without blocking the caller.
select {
case s.pause <- struct{}{}:
default:
}
return nil
}
@@ -176,9 +181,12 @@ func (s *Stream) process() {
return
}
int16Buffer := make([]int16, frameSize)
s.l.Lock()
volume := s.Volume
s.l.Unlock()
for i := range int16Buffer {
float := float32(int16(binary.LittleEndian.Uint16(byteBuffer[i*2 : (i+1)*2])))
int16Buffer[i] = int16(s.Volume * float)
int16Buffer[i] = int16(volume * float)
}
atomic.AddInt64(&s.elapsed, int64(interval))
outgoing <- gumble.AudioBuffer(int16Buffer)
+22
View File
@@ -0,0 +1,22 @@
package gumbleffmpeg
import (
"testing"
"time"
)
// Regression: Pause sent on an unbuffered channel after the process had
// exited, leaving callers blocked forever.
func TestPauseDoesNotBlockWhenProcessHasExited(t *testing.T) {
s := &Stream{state: StatePlaying, pause: make(chan struct{}, 1)}
done := make(chan error, 1)
go func() { done <- s.Pause() }()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("Pause blocked after process exit")
}
}