Skip to content

Commit 9cdc59e

Browse files
authored
Fix race condition causing "Writer is not bound to a WritableStream" error in Silero VAD (#786)
1 parent 9b50d88 commit 9cdc59e

File tree

3 files changed

+81
-46
lines changed

3 files changed

+81
-46
lines changed

.changeset/soft-memes-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents-plugin-silero': patch
3+
---
4+
5+
Fix race condition causing "Writer is not bound to a WritableStream" error in Silero VAD

agents/src/vad.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,24 @@ export abstract class VADStream implements AsyncIterableIterator<VADEvent> {
177177
}
178178
}
179179

180+
/**
181+
* Safely send a VAD event to the output stream, handling writer release errors during shutdown.
182+
* @returns true if the event was sent, false if the stream is closing
183+
* @throws Error if an unexpected error occurs
184+
*/
185+
protected sendVADEvent(event: VADEvent): boolean {
186+
if (this.closed) {
187+
return false;
188+
}
189+
190+
try {
191+
this.outputWriter.write(event);
192+
return true;
193+
} catch (e) {
194+
throw e;
195+
}
196+
}
197+
180198
updateInputStream(audioStream: ReadableStream<AudioFrame>) {
181199
this.deferredInputStream.setSource(audioStream);
182200
}

plugins/silero/src/vad.ts

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -260,26 +260,30 @@ export class VADStream extends baseStream {
260260
pubSilenceDuration += windowDuration;
261261
}
262262

263-
this.outputWriter.write({
264-
type: VADEventType.INFERENCE_DONE,
265-
samplesIndex: pubCurrentSample,
266-
timestamp: pubTimestamp,
267-
silenceDuration: pubSilenceDuration,
268-
speechDuration: pubSpeechDuration,
269-
probability: p,
270-
inferenceDuration,
271-
frames: [
272-
new AudioFrame(
273-
inputFrame.data.subarray(0, toCopyInt),
274-
this.#inputSampleRate,
275-
1,
276-
toCopyInt,
277-
),
278-
],
279-
speaking: pubSpeaking,
280-
rawAccumulatedSilence: silenceThresholdDuration,
281-
rawAccumulatedSpeech: speechThresholdDuration,
282-
});
263+
if (
264+
!this.sendVADEvent({
265+
type: VADEventType.INFERENCE_DONE,
266+
samplesIndex: pubCurrentSample,
267+
timestamp: pubTimestamp,
268+
silenceDuration: pubSilenceDuration,
269+
speechDuration: pubSpeechDuration,
270+
probability: p,
271+
inferenceDuration,
272+
frames: [
273+
new AudioFrame(
274+
inputFrame.data.subarray(0, toCopyInt),
275+
this.#inputSampleRate,
276+
1,
277+
toCopyInt,
278+
),
279+
],
280+
speaking: pubSpeaking,
281+
rawAccumulatedSilence: silenceThresholdDuration,
282+
rawAccumulatedSpeech: speechThresholdDuration,
283+
})
284+
) {
285+
continue;
286+
}
283287

284288
const resetWriteCursor = () => {
285289
if (!this.#speechBuffer) throw new Error('speechBuffer is empty');
@@ -314,19 +318,23 @@ export class VADStream extends baseStream {
314318
pubSilenceDuration = 0;
315319
pubSpeechDuration = speechThresholdDuration;
316320

317-
this.outputWriter.write({
318-
type: VADEventType.START_OF_SPEECH,
319-
samplesIndex: pubCurrentSample,
320-
timestamp: pubTimestamp,
321-
silenceDuration: pubSilenceDuration,
322-
speechDuration: pubSpeechDuration,
323-
probability: p,
324-
inferenceDuration,
325-
frames: [copySpeechBuffer()],
326-
speaking: pubSpeaking,
327-
rawAccumulatedSilence: 0,
328-
rawAccumulatedSpeech: 0,
329-
});
321+
if (
322+
!this.sendVADEvent({
323+
type: VADEventType.START_OF_SPEECH,
324+
samplesIndex: pubCurrentSample,
325+
timestamp: pubTimestamp,
326+
silenceDuration: pubSilenceDuration,
327+
speechDuration: pubSpeechDuration,
328+
probability: p,
329+
inferenceDuration,
330+
frames: [copySpeechBuffer()],
331+
speaking: pubSpeaking,
332+
rawAccumulatedSilence: 0,
333+
rawAccumulatedSpeech: 0,
334+
})
335+
) {
336+
continue;
337+
}
330338
}
331339
} else {
332340
silenceThresholdDuration += windowDuration;
@@ -341,19 +349,23 @@ export class VADStream extends baseStream {
341349
pubSpeechDuration = 0;
342350
pubSilenceDuration = silenceThresholdDuration;
343351

344-
this.outputWriter.write({
345-
type: VADEventType.END_OF_SPEECH,
346-
samplesIndex: pubCurrentSample,
347-
timestamp: pubTimestamp,
348-
silenceDuration: pubSilenceDuration,
349-
speechDuration: pubSpeechDuration,
350-
probability: p,
351-
inferenceDuration,
352-
frames: [copySpeechBuffer()],
353-
speaking: pubSpeaking,
354-
rawAccumulatedSilence: 0,
355-
rawAccumulatedSpeech: 0,
356-
});
352+
if (
353+
!this.sendVADEvent({
354+
type: VADEventType.END_OF_SPEECH,
355+
samplesIndex: pubCurrentSample,
356+
timestamp: pubTimestamp,
357+
silenceDuration: pubSilenceDuration,
358+
speechDuration: pubSpeechDuration,
359+
probability: p,
360+
inferenceDuration,
361+
frames: [copySpeechBuffer()],
362+
speaking: pubSpeaking,
363+
rawAccumulatedSilence: 0,
364+
rawAccumulatedSpeech: 0,
365+
})
366+
) {
367+
continue;
368+
}
357369

358370
resetWriteCursor();
359371
}

0 commit comments

Comments
 (0)