Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions sender/frame_buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,34 @@ func (f *FrameBuffer) ResetInitialized() {
}

// Read returns the next available frame from the buffer.
// When initialized (normal operation), returns immediately with ErrNoFrameAvailable
// if no frame is ready. When not initialized (encoder init), blocks up to 100ms
// and returns a black frame for codec property detection.
// When initialized (normal operation), it returns immediately with
// ErrNoFrameAvailable if no frame is ready. The vpx encoder's Read holds
// its internal mutex across the inner Read call, so blocking here would
// also block concurrent DynamicQPControl bitrate updates that try to take
// the same mutex. runEncodeLoop handles ErrNoFrameAvailable by sleeping
// briefly before retrying. When not initialized (encoder init), it blocks
// up to 100ms and returns a black frame for codec property detection.
//
// Side effect: stores the popped frame's captureTSUs for LastCaptureTSUs.
// Black-frame timeouts preserve the previous value so encoders with
// lookahead can still correlate the previously-read real frame.
func (f *FrameBuffer) Read() (image.Image, func(), error) {
if f.initialized {
// Check closeChan first so a closed buffer always reports
// ErrBufferClosed instead of racing with the default branch below
// (Go's select picks ready cases pseudo-randomly).
select {
case <-f.closeChan:
return nil, func() {}, ErrBufferClosed
default:
}
// Non-blocking fast path for normal operation.
select {
case fm := <-f.frameChan:
f.lastCaptureTSUs.Store(fm.captureTSUs)
f.lastDequeueWallUs.Store(time.Now().UnixMicro())

return fm.img, func() {}, nil
case <-f.closeChan:
return nil, func() {}, ErrBufferClosed
default:
return nil, func() {}, ErrNoFrameAvailable
}
Expand Down
7 changes: 3 additions & 4 deletions sender/frame_buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,9 @@ func TestFrameBuffer_ReadWithoutFrames(t *testing.T) {
fb := NewFrameBuffer(640, 480)
defer func() { _ = fb.Close() }()

// Mark as initialized so we don't get black frames
// Mark as initialized so we take the non-blocking fast path.
fb.SetInitialized()

// Try to read without sending any frames
// This should return ErrNoFrameAvailable immediately (non-blocking)
img, release, err := fb.Read()
assert.Nil(t, img)
assert.NotNil(t, release)
Expand Down Expand Up @@ -165,7 +163,8 @@ func TestFrameBuffer_ReadInitializedAfterClose(t *testing.T) {
fb := NewFrameBuffer(640, 480)
fb.SetInitialized()

// Close, then read — fast path should detect closeChan.
// Close, then read — the closeChan precedence check should win over
// the default branch in the non-blocking select.
err := fb.Close()
require.NoError(t, err)

Expand Down
Loading
Loading