Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 15 additions & 0 deletions .changeset/fix-recorder-capture-finish-race.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@livekit/agents': patch
---

Fix a deadlock where a recorder-wrapped audio output could leave `waitForPlayout` stranded when
an interrupt arrived before the recorder had registered its segment. `RecorderAudioOutput` now
registers its own segment before forwarding a frame downstream, and attributes each playback
finish to the segment it belongs to instead of relying on a global counter.

A recorded segment is also timestamped when it opens rather than when the wrapped output accepts
its first frame, so a finish that lands while that frame is parked no longer clamps the segment's
playback position to zero and drop the audio the sink reported as played. And a segment whose
downstream capture throws now releases the capture latch on both the recorder and the wrapped
output, so a caller that retries after a transient rejection is no longer rejected forever with
`recorder capture has no active segment`.
13 changes: 13 additions & 0 deletions agents/src/voice/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ export abstract class AudioOutput extends EventEmitter {
this._capturing = false;
}

/**
* Forget the segment currently being captured, without treating it as a flush boundary.
*
* For an output whose open segment was abandoned rather than flushed — e.g. a capture threw and
* the segment has already been reported finished. Without this the output keeps believing a
* segment is open, so the next `captureFrame` silently joins a segment that no longer exists
* instead of counting a new one.
* @internal
*/
abandonOpenSegment(): void {
this._capturing = false;
}

/**
* Clear the buffer, stopping playback immediately
*/
Expand Down
Loading
Loading