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
5 changes: 5 additions & 0 deletions .changeset/cold-walls-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/rtc-node': patch
---

Dispose native handles on audio/video stream EOS to prevent FD leaks
19 changes: 17 additions & 2 deletions packages/livekit-rtc/src/audio_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface NoiseCancellationOptions {
class AudioStreamSource implements UnderlyingSource<AudioFrame> {
private controller?: ReadableStreamDefaultController<AudioFrame>;
private ffiHandle: FfiHandle;
private disposed = false;
private sampleRate: number;
private numChannels: number;
private legacyNcOptions?: NoiseCancellationOptions;
Expand Down Expand Up @@ -106,7 +107,15 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
case 'eos':
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
this.controller.close();
this.frameProcessor?.close();
// Dispose the native handle so the FD is released on stream end,
// not just when cancel() is called explicitly by the consumer.
// Guard against double-dispose if cancel() is called after EOS
// while buffered frames are still in the ReadableStream queue.
if (!this.disposed) {
this.disposed = true;
this.ffiHandle.dispose();
this.frameProcessor?.close();
}
break;
}
};
Expand All @@ -117,7 +126,13 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {

cancel() {
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
this.ffiHandle.dispose();
if (!this.disposed) {
this.disposed = true;
this.ffiHandle.dispose();
// Also close the frame processor on cancel for symmetry with the EOS path,
// so resources are released regardless of how the stream ends.
this.frameProcessor?.close();
}
}
}

Expand Down
14 changes: 13 additions & 1 deletion packages/livekit-rtc/src/video_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type VideoFrameEvent = {
class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {
private controller?: ReadableStreamDefaultController<VideoFrameEvent>;
private ffiHandle: FfiHandle;
private disposed = false;

constructor(track: Track) {
const req = new NewVideoStreamRequest({
Expand Down Expand Up @@ -65,6 +66,14 @@ class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {
case 'eos':
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
this.controller.close();
// Dispose the native handle so the FD is released on stream end,
// not just when cancel() is called explicitly by the consumer.
// Guard against double-dispose if cancel() is called after EOS
// while buffered frames are still in the ReadableStream queue.
if (!this.disposed) {
this.disposed = true;
this.ffiHandle.dispose();
}
break;
}
};
Expand All @@ -75,7 +84,10 @@ class VideoStreamSource implements UnderlyingSource<VideoFrameEvent> {

cancel() {
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
this.ffiHandle.dispose();
if (!this.disposed) {
this.disposed = true;
this.ffiHandle.dispose();
}
}
}

Expand Down
Loading