Skip to content

Commit 172d345

Browse files
authored
fix: ensure all tracks are stopped when disposing a Publisher (#1677)
Continuation on #1676. We were still leaking tracks in iOS Safari. ### Implementation notes Aggressively keep track of cloned tracks in Publisher and ensure all of them are stopped when it is disposed of.
1 parent 3377abf commit 172d345

File tree

2 files changed

+42
-8
lines changed

2 files changed

+42
-8
lines changed

packages/client/src/rtc/Publisher.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type PublisherConstructorOpts = BasePeerConnectionOpts & {
3131
*/
3232
export class Publisher extends BasePeerConnection {
3333
private readonly transceiverCache = new TransceiverCache();
34+
private readonly clonedTracks = new Set<MediaStreamTrack>();
3435
private publishOptions: PublishOption[];
3536

3637
/**
@@ -79,6 +80,7 @@ export class Publisher extends BasePeerConnection {
7980
dispose() {
8081
super.dispose();
8182
this.stopAllTracks();
83+
this.clonedTracks.clear();
8284
}
8385

8486
/**
@@ -100,15 +102,15 @@ export class Publisher extends BasePeerConnection {
100102

101103
// create a clone of the track as otherwise the same trackId will
102104
// appear in the SDP in multiple transceivers
103-
const trackToPublish = track.clone();
105+
const trackToPublish = this.cloneTrack(track);
104106

105107
const transceiver = this.transceiverCache.get(publishOption);
106108
if (!transceiver) {
107109
this.addTransceiver(trackToPublish, publishOption);
108110
} else {
109111
const previousTrack = transceiver.sender.track;
110112
await transceiver.sender.replaceTrack(trackToPublish);
111-
previousTrack?.stop();
113+
this.stopTrack(previousTrack);
112114
}
113115
}
114116
};
@@ -153,7 +155,7 @@ export class Publisher extends BasePeerConnection {
153155

154156
// take the track from the existing transceiver for the same track type,
155157
// clone it and publish it with the new publish options
156-
const track = item.transceiver.sender.track!.clone();
158+
const track = this.cloneTrack(item.transceiver.sender.track!);
157159
this.addTransceiver(track, publishOption);
158160
}
159161

@@ -167,7 +169,7 @@ export class Publisher extends BasePeerConnection {
167169
);
168170
if (hasPublishOption) continue;
169171
// it is safe to stop the track here, it is a clone
170-
transceiver.sender.track?.stop();
172+
this.stopTrack(transceiver.sender.track);
171173
await transceiver.sender.replaceTrack(null);
172174
}
173175
};
@@ -209,7 +211,7 @@ export class Publisher extends BasePeerConnection {
209211
for (const item of this.transceiverCache.items()) {
210212
const { publishOption, transceiver } = item;
211213
if (!trackTypes.includes(publishOption.trackType)) continue;
212-
transceiver.sender.track?.stop();
214+
this.stopTrack(transceiver.sender.track);
213215
}
214216
};
215217

@@ -218,7 +220,10 @@ export class Publisher extends BasePeerConnection {
218220
*/
219221
stopAllTracks = () => {
220222
for (const { transceiver } of this.transceiverCache.items()) {
221-
transceiver.sender.track?.stop();
223+
this.stopTrack(transceiver.sender.track);
224+
}
225+
for (const track of this.clonedTracks) {
226+
this.stopTrack(track);
222227
}
223228
};
224229

@@ -433,4 +438,16 @@ export class Publisher extends BasePeerConnection {
433438
publishOptionId: publishOption.id,
434439
};
435440
};
441+
442+
private cloneTrack = (track: MediaStreamTrack): MediaStreamTrack => {
443+
const clone = track.clone();
444+
this.clonedTracks.add(clone);
445+
return clone;
446+
};
447+
448+
private stopTrack = (track: MediaStreamTrack | null | undefined) => {
449+
if (!track) return;
450+
track.stop();
451+
this.clonedTracks.delete(track);
452+
};
436453
}

packages/client/src/rtc/__tests__/Publisher.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('Publisher', () => {
116116
},
117117
],
118118
});
119+
expect(publisher['clonedTracks'].size).toBe(1);
119120
});
120121

121122
it('should update an existing transceiver for a new track', async () => {
@@ -597,10 +598,22 @@ describe('Publisher', () => {
597598
);
598599
vi.spyOn(inactiveTrack, 'readyState', 'get').mockReturnValue('ended');
599600

601+
const audioTransceiver = new RTCRtpTransceiver();
602+
const audioTrack = new MediaStreamTrack();
603+
vi.spyOn(audioTrack, 'kind', 'get').mockReturnValue('audio');
604+
vi.spyOn(audioTrack, 'enabled', 'get').mockReturnValue(true);
605+
vi.spyOn(audioTransceiver.sender, 'track', 'get').mockReturnValue(
606+
audioTrack,
607+
);
608+
600609
// @ts-expect-error incomplete data
601610
cache.add({ trackType: TrackType.VIDEO, id: 1 }, transceiver);
602611
// @ts-expect-error incomplete data
603612
cache.add({ trackType: TrackType.VIDEO, id: 2 }, inactiveTransceiver);
613+
// @ts-expect-error incomplete data
614+
cache.add({ trackType: TrackType.AUDIO, id: 3 }, audioTransceiver);
615+
616+
publisher['clonedTracks'].add(track).add(inactiveTrack).add(audioTrack);
604617
});
605618

606619
it('negotiate should set up the local and remote descriptions', async () => {
@@ -666,13 +679,13 @@ describe('Publisher', () => {
666679

667680
it('getPublishedTracks returns the published tracks', () => {
668681
const tracks = publisher.getPublishedTracks();
669-
expect(tracks).toHaveLength(1);
682+
expect(tracks).toHaveLength(2);
670683
expect(tracks[0].readyState).toBe('live');
671684
});
672685

673686
it('getAnnouncedTracks should return all tracks', () => {
674687
const trackInfos = publisher.getAnnouncedTracks('');
675-
expect(trackInfos).toHaveLength(2);
688+
expect(trackInfos).toHaveLength(3);
676689
expect(trackInfos[0].muted).toBe(false);
677690
expect(trackInfos[0].mid).toBe('0');
678691
expect(trackInfos[1].muted).toBe(true);
@@ -701,15 +714,19 @@ describe('Publisher', () => {
701714
it('stopTracks should stop tracks', () => {
702715
const track = cache['cache'][0].transceiver.sender.track;
703716
vi.spyOn(track, 'stop');
717+
expect(publisher['clonedTracks'].size).toBe(3);
704718
publisher.stopTracks(TrackType.VIDEO);
705719
expect(track!.stop).toHaveBeenCalled();
720+
expect(publisher['clonedTracks'].size).toBe(1);
706721
});
707722

708723
it('stopAllTracks should stop all tracks', () => {
709724
const track = cache['cache'][0].transceiver.sender.track;
710725
vi.spyOn(track, 'stop');
726+
expect(publisher['clonedTracks'].size).toBe(3);
711727
publisher.stopAllTracks();
712728
expect(track!.stop).toHaveBeenCalled();
729+
expect(publisher['clonedTracks'].size).toBe(0);
713730
});
714731
});
715732
});

0 commit comments

Comments
 (0)