@stream-io/video-client 1.16.3 → 1.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,7 @@ export declare abstract class BasePeerConnection {
34
34
  /**
35
35
  * Disposes the `RTCPeerConnection` instance.
36
36
  */
37
- dispose: () => void;
37
+ dispose(): void;
38
38
  /**
39
39
  * Detaches the event handlers from the `RTCPeerConnection`.
40
40
  */
@@ -10,6 +10,7 @@ export type PublisherConstructorOpts = BasePeerConnectionOpts & {
10
10
  */
11
11
  export declare class Publisher extends BasePeerConnection {
12
12
  private readonly transceiverCache;
13
+ private readonly clonedTracks;
13
14
  private publishOptions;
14
15
  /**
15
16
  * Constructs a new `Publisher` instance.
@@ -21,6 +22,10 @@ export declare class Publisher extends BasePeerConnection {
21
22
  * instance with a new one (in case of migration).
22
23
  */
23
24
  detachEventHandlers(): void;
25
+ /**
26
+ * Disposes this Publisher instance.
27
+ */
28
+ dispose(): void;
24
29
  /**
25
30
  * Starts publishing the given track of the given media stream.
26
31
  *
@@ -53,6 +58,10 @@ export declare class Publisher extends BasePeerConnection {
53
58
  * Stops the cloned track that is being published to the SFU.
54
59
  */
55
60
  stopTracks: (...trackTypes: TrackType[]) => void;
61
+ /**
62
+ * Stops all the cloned tracks that are being published to the SFU.
63
+ */
64
+ stopAllTracks: () => void;
56
65
  private changePublishQuality;
57
66
  /**
58
67
  * Restarts the ICE connection and renegotiates with the SFU.
@@ -84,4 +93,6 @@ export declare class Publisher extends BasePeerConnection {
84
93
  * Converts the given transceiver to a `TrackInfo` object.
85
94
  */
86
95
  private toTrackInfo;
96
+ private cloneTrack;
97
+ private stopTrack;
87
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.16.3",
3
+ "version": "1.16.5",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -75,12 +75,12 @@ export abstract class BasePeerConnection {
75
75
  /**
76
76
  * Disposes the `RTCPeerConnection` instance.
77
77
  */
78
- dispose = () => {
78
+ dispose() {
79
79
  this.onUnrecoverableError = undefined;
80
80
  this.isDisposed = true;
81
81
  this.detachEventHandlers();
82
82
  this.pc.close();
83
- };
83
+ }
84
84
 
85
85
  /**
86
86
  * Detaches the event handlers from the `RTCPeerConnection`.
@@ -31,6 +31,7 @@ export type PublisherConstructorOpts = BasePeerConnectionOpts & {
31
31
  */
32
32
  export class Publisher extends BasePeerConnection {
33
33
  private readonly transceiverCache = new TransceiverCache();
34
+ private readonly clonedTracks = new Set<MediaStreamTrack>();
34
35
  private publishOptions: PublishOption[];
35
36
 
36
37
  /**
@@ -73,6 +74,15 @@ export class Publisher extends BasePeerConnection {
73
74
  withCancellation('publisher.negotiate', () => Promise.resolve());
74
75
  }
75
76
 
77
+ /**
78
+ * Disposes this Publisher instance.
79
+ */
80
+ dispose() {
81
+ super.dispose();
82
+ this.stopAllTracks();
83
+ this.clonedTracks.clear();
84
+ }
85
+
76
86
  /**
77
87
  * Starts publishing the given track of the given media stream.
78
88
  *
@@ -92,13 +102,15 @@ export class Publisher extends BasePeerConnection {
92
102
 
93
103
  // create a clone of the track as otherwise the same trackId will
94
104
  // appear in the SDP in multiple transceivers
95
- const trackToPublish = track.clone();
105
+ const trackToPublish = this.cloneTrack(track);
96
106
 
97
107
  const transceiver = this.transceiverCache.get(publishOption);
98
108
  if (!transceiver) {
99
109
  this.addTransceiver(trackToPublish, publishOption);
100
110
  } else {
111
+ const previousTrack = transceiver.sender.track;
101
112
  await transceiver.sender.replaceTrack(trackToPublish);
113
+ this.stopTrack(previousTrack);
102
114
  }
103
115
  }
104
116
  };
@@ -143,7 +155,7 @@ export class Publisher extends BasePeerConnection {
143
155
 
144
156
  // take the track from the existing transceiver for the same track type,
145
157
  // clone it and publish it with the new publish options
146
- const track = item.transceiver.sender.track!.clone();
158
+ const track = this.cloneTrack(item.transceiver.sender.track!);
147
159
  this.addTransceiver(track, publishOption);
148
160
  }
149
161
 
@@ -157,7 +169,7 @@ export class Publisher extends BasePeerConnection {
157
169
  );
158
170
  if (hasPublishOption) continue;
159
171
  // it is safe to stop the track here, it is a clone
160
- transceiver.sender.track?.stop();
172
+ this.stopTrack(transceiver.sender.track);
161
173
  await transceiver.sender.replaceTrack(null);
162
174
  }
163
175
  };
@@ -199,7 +211,19 @@ export class Publisher extends BasePeerConnection {
199
211
  for (const item of this.transceiverCache.items()) {
200
212
  const { publishOption, transceiver } = item;
201
213
  if (!trackTypes.includes(publishOption.trackType)) continue;
202
- transceiver.sender.track?.stop();
214
+ this.stopTrack(transceiver.sender.track);
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Stops all the cloned tracks that are being published to the SFU.
220
+ */
221
+ stopAllTracks = () => {
222
+ for (const { transceiver } of this.transceiverCache.items()) {
223
+ this.stopTrack(transceiver.sender.track);
224
+ }
225
+ for (const track of this.clonedTracks) {
226
+ this.stopTrack(track);
203
227
  }
204
228
  };
205
229
 
@@ -414,4 +438,16 @@ export class Publisher extends BasePeerConnection {
414
438
  publishOptionId: publishOption.id,
415
439
  };
416
440
  };
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
+ };
417
453
  }
@@ -116,6 +116,7 @@ describe('Publisher', () => {
116
116
  },
117
117
  ],
118
118
  });
119
+ expect(publisher['clonedTracks'].size).toBe(1);
119
120
  });
120
121
 
121
122
  it('should update an existing transceiver for a new track', async () => {
@@ -124,6 +125,8 @@ describe('Publisher', () => {
124
125
  vi.spyOn(track, 'clone').mockReturnValue(clone);
125
126
 
126
127
  const transceiver = new RTCRtpTransceiver();
128
+ // @ts-ignore test setup
129
+ transceiver.sender.track = track;
127
130
  publisher['transceiverCache'].add(
128
131
  publisher['publishOptions'][0],
129
132
  transceiver,
@@ -134,6 +137,7 @@ describe('Publisher', () => {
134
137
  expect(track.clone).toHaveBeenCalled();
135
138
  expect(publisher['pc'].addTransceiver).not.toHaveBeenCalled();
136
139
  expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(clone);
140
+ expect(track.stop).toHaveBeenCalled();
137
141
  });
138
142
  });
139
143
 
@@ -594,10 +598,22 @@ describe('Publisher', () => {
594
598
  );
595
599
  vi.spyOn(inactiveTrack, 'readyState', 'get').mockReturnValue('ended');
596
600
 
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
+
597
609
  // @ts-expect-error incomplete data
598
610
  cache.add({ trackType: TrackType.VIDEO, id: 1 }, transceiver);
599
611
  // @ts-expect-error incomplete data
600
612
  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);
601
617
  });
602
618
 
603
619
  it('negotiate should set up the local and remote descriptions', async () => {
@@ -663,13 +679,13 @@ describe('Publisher', () => {
663
679
 
664
680
  it('getPublishedTracks returns the published tracks', () => {
665
681
  const tracks = publisher.getPublishedTracks();
666
- expect(tracks).toHaveLength(1);
682
+ expect(tracks).toHaveLength(2);
667
683
  expect(tracks[0].readyState).toBe('live');
668
684
  });
669
685
 
670
686
  it('getAnnouncedTracks should return all tracks', () => {
671
687
  const trackInfos = publisher.getAnnouncedTracks('');
672
- expect(trackInfos).toHaveLength(2);
688
+ expect(trackInfos).toHaveLength(3);
673
689
  expect(trackInfos[0].muted).toBe(false);
674
690
  expect(trackInfos[0].mid).toBe('0');
675
691
  expect(trackInfos[1].muted).toBe(true);
@@ -698,8 +714,19 @@ describe('Publisher', () => {
698
714
  it('stopTracks should stop tracks', () => {
699
715
  const track = cache['cache'][0].transceiver.sender.track;
700
716
  vi.spyOn(track, 'stop');
717
+ expect(publisher['clonedTracks'].size).toBe(3);
701
718
  publisher.stopTracks(TrackType.VIDEO);
702
719
  expect(track!.stop).toHaveBeenCalled();
720
+ expect(publisher['clonedTracks'].size).toBe(1);
721
+ });
722
+
723
+ it('stopAllTracks should stop all tracks', () => {
724
+ const track = cache['cache'][0].transceiver.sender.track;
725
+ vi.spyOn(track, 'stop');
726
+ expect(publisher['clonedTracks'].size).toBe(3);
727
+ publisher.stopAllTracks();
728
+ expect(track!.stop).toHaveBeenCalled();
729
+ expect(publisher['clonedTracks'].size).toBe(0);
703
730
  });
704
731
  });
705
732
  });
@@ -6,7 +6,7 @@ const RTCPeerConnectionMock = vi.fn((): Partial<RTCPeerConnection> => {
6
6
  addIceCandidate: vi.fn(),
7
7
  removeEventListener: vi.fn(),
8
8
  getTransceivers: vi.fn(),
9
- addTransceiver: vi.fn(),
9
+ addTransceiver: vi.fn().mockReturnValue(new RTCRtpTransceiverMock()),
10
10
  getConfiguration: vi.fn(),
11
11
  setConfiguration: vi.fn(),
12
12
  createOffer: vi.fn().mockResolvedValue({}),