Skip to content

Commit 4a5494c

Browse files
committed
feat: port in updates from livekit/python-sdks#679
1 parent c1e6f43 commit 4a5494c

4 files changed

Lines changed: 46 additions & 10 deletions

File tree

packages/livekit-rtc/src/audio_stream.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import type { Track } from './track.js';
1414
export interface AudioStreamOptions {
1515
noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;
1616
/**
17-
* If true and `noiseCancellation` is a {@link FrameProcessor}, leaves the
18-
* processor open when the stream closes so the same processor can be reused
19-
* with another {@link AudioStream}. Defaults to `false`.
17+
* When the audio stream closes, whether to run the {@link FrameProcessor}'s
18+
* `close()` method. If `false`, the processor is left open so it can be
19+
* reused with another {@link AudioStream}. Only relevant when
20+
* `noiseCancellation` is a {@link FrameProcessor}. Defaults to `true`.
2021
*/
21-
noiseCancellationLeaveOpen?: boolean;
22+
autoCloseNoiseCancellation?: boolean;
2223
sampleRate?: number;
2324
numChannels?: number;
2425
frameSizeMs?: number;
@@ -38,7 +39,7 @@ export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
3839
private numChannels: number;
3940
private legacyNcOptions?: NoiseCancellationOptions;
4041
private frameProcessor: FrameProcessor<AudioFrame> | null = null;
41-
private leaveProcessorOpen = false;
42+
private autoCloseProcessor = true;
4243
private frameSizeMs?: number;
4344
private track: Track;
4445

@@ -53,7 +54,7 @@ export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
5354
this.numChannels = sampleRateOrOptions.numChannels ?? 1;
5455
if (isFrameProcessor(sampleRateOrOptions.noiseCancellation)) {
5556
this.frameProcessor = sampleRateOrOptions.noiseCancellation;
56-
this.leaveProcessorOpen = sampleRateOrOptions.noiseCancellationLeaveOpen ?? false;
57+
this.autoCloseProcessor = sampleRateOrOptions.autoCloseNoiseCancellation ?? true;
5758
} else {
5859
this.legacyNcOptions = sampleRateOrOptions.noiseCancellation;
5960
}
@@ -131,7 +132,7 @@ export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
131132
this.disposed = true;
132133
this.track.unregisterAudioStream(this);
133134
this.ffiHandle.dispose();
134-
if (this.frameProcessor && !this.leaveProcessorOpen) {
135+
if (this.frameProcessor && this.autoCloseProcessor) {
135136
this.frameProcessor.close();
136137
}
137138
}
@@ -151,7 +152,7 @@ export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
151152
this.ffiHandle.dispose();
152153
// Also close the frame processor on cancel for symmetry with the EOS path,
153154
// so resources are released regardless of how the stream ends.
154-
if (this.frameProcessor && !this.leaveProcessorOpen) {
155+
if (this.frameProcessor && this.autoCloseProcessor) {
155156
this.frameProcessor.close();
156157
}
157158
}

packages/livekit-rtc/src/participant.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,15 @@ export class LocalParticipant extends Participant {
767767

768768
const pub = this.trackPublications.get(trackSid);
769769
if (pub) {
770+
// Clear the processor's room context here too: this path races the
771+
// localTrackUnpublished room event, and whichever loses finds the
772+
// publication already gone and skips its own setRoom(null). Calling it
773+
// from both paths guarantees the processor is cleared (and the
774+
// tokenRefreshed listener detached); setRoom(null) is idempotent, so a
775+
// double-clear when this path wins is safe.
776+
if (pub.track) {
777+
pub.track.setRoom(null);
778+
}
770779
pub.track = undefined;
771780
}
772781
this.trackPublications.delete(trackSid);

packages/livekit-rtc/src/room.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,11 +553,21 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
553553
this.emit(RoomEvent.LocalTrackPublished, publication!, this.localParticipant);
554554
} else if (ev.case == 'localTrackUnpublished') {
555555
const publication = this.localParticipant.trackPublications.get(ev.value.publicationSid!);
556-
if (publication?.track) {
557-
publication.track.setRoom(null);
556+
const track = publication?.track;
557+
if (track) {
558+
track.setRoom(null);
558559
}
559560
this.localParticipant.trackPublications.delete(ev.value.publicationSid!);
561+
// Emit while `publication.track` is still set, preserving the pre-existing
562+
// payload for callbacks. The handler is synchronous, so nulling the track
563+
// right after still completes before any other turn can observe it.
560564
this.emit(RoomEvent.LocalTrackUnpublished, publication!, this.localParticipant!);
565+
// Mirror trackUnsubscribed: drop the publication's track reference. This
566+
// also makes unpublishTrack's own setRoom(null) a no-op when it loses the
567+
// race (its `pub.track` guard short-circuits), avoiding a redundant clear.
568+
if (track && publication) {
569+
publication.track = undefined;
570+
}
561571
} else if ((ev.case as string) == 'localTrackRepublished') {
562572
const value = (ev as any).value;
563573
const previousSid: string = value.previousSid!;
@@ -567,6 +577,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
567577
publication.updateInfo(newInfo);
568578
this.localParticipant.trackPublications.delete(previousSid);
569579
this.localParticipant.trackPublications.set(publication.sid!, publication);
580+
if (publication.track?.info) {
581+
// Keep the local-track invariant (track.sid == publication.sid, set at
582+
// publishTrack) intact across republish, then re-push metadata so any
583+
// attached FrameProcessor learns the new publication SID / credentials.
584+
// setRoom with the same room is a no-op for the tokenRefreshed listener
585+
// but re-fans the metadata to every registered AudioStream.
586+
publication.track.info.sid = publication.sid;
587+
publication.track.setRoom(this);
588+
}
570589
this.emit(RoomEvent.LocalTrackRepublished, publication, previousSid, this.localParticipant);
571590
} else {
572591
log.warn(`RoomEvent.LocalTrackRepublished: previous publication not found: ${previousSid}`);

packages/livekit-rtc/src/track.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export abstract class Track {
5656
/** @internal */
5757
setRoom(room: Room | null): void {
5858
const oldRoom = this.resolveRoom();
59+
if (!oldRoom && !room) {
60+
// Already roomless — nothing to detach and nothing to re-clear. Without
61+
// this guard a second setRoom(null) (e.g. the unpublishTrack /
62+
// localTrackUnpublished race calling it from both paths) would re-fire
63+
// onStreamInfoCleared / onCredentialsCleared on every registered processor.
64+
return;
65+
}
5966
if (oldRoom && oldRoom !== room) {
6067
oldRoom.off('tokenRefreshed', this.onRoomTokenRefreshed);
6168
}

0 commit comments

Comments
 (0)