@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +48 -15
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +48 -15
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +48 -15
- package/dist/index.es.js.map +1 -1
- package/dist/src/rtc/BasePeerConnection.d.ts +1 -1
- package/dist/src/rtc/Publisher.d.ts +11 -0
- package/package.json +1 -1
- package/src/rtc/BasePeerConnection.ts +2 -2
- package/src/rtc/Publisher.ts +40 -4
- package/src/rtc/__tests__/Publisher.test.ts +29 -2
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -1
|
@@ -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
|
@@ -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`.
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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({}),
|