@stream-io/video-client 1.13.1 → 1.15.0
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 +1704 -1762
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1706 -1780
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1704 -1762
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +61 -30
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/coordinator/index.d.ts +904 -515
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +47 -5
- package/dist/src/store/rxUtils.d.ts +15 -1
- package/dist/src/types.d.ts +26 -22
- package/package.json +1 -1
- package/src/Call.ts +310 -271
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +36 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/coordinator/index.ts +902 -514
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/store/CallState.ts +115 -5
- package/src/store/__tests__/CallState.test.ts +101 -0
- package/src/store/rxUtils.ts +23 -1
- package/src/types.ts +27 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
|
@@ -5,10 +5,16 @@ import { Publisher } from '../Publisher';
|
|
|
5
5
|
import { CallState } from '../../store';
|
|
6
6
|
import { StreamSfuClient } from '../../StreamSfuClient';
|
|
7
7
|
import { DispatchableMessage, Dispatcher } from '../Dispatcher';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
PeerType,
|
|
10
|
+
PublishOption,
|
|
11
|
+
TrackInfo,
|
|
12
|
+
TrackType,
|
|
13
|
+
} from '../../gen/video/sfu/models/models';
|
|
9
14
|
import { SfuEvent } from '../../gen/video/sfu/event/events';
|
|
10
15
|
import { IceTrickleBuffer } from '../IceTrickleBuffer';
|
|
11
16
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
17
|
+
import { TransceiverCache } from '../TransceiverCache';
|
|
12
18
|
|
|
13
19
|
vi.mock('../../StreamSfuClient', () => {
|
|
14
20
|
console.log('MOCKING StreamSfuClient');
|
|
@@ -17,22 +23,6 @@ vi.mock('../../StreamSfuClient', () => {
|
|
|
17
23
|
};
|
|
18
24
|
});
|
|
19
25
|
|
|
20
|
-
vi.mock('../codecs', async () => {
|
|
21
|
-
const codecs = await vi.importActual('../codecs');
|
|
22
|
-
return {
|
|
23
|
-
getPreferredCodecs: vi.fn((): RTCRtpCodecCapability[] => [
|
|
24
|
-
{
|
|
25
|
-
channels: 1,
|
|
26
|
-
clockRate: 48000,
|
|
27
|
-
mimeType: 'video/h264',
|
|
28
|
-
sdpFmtpLine: 'profile-level-id=42e01f',
|
|
29
|
-
},
|
|
30
|
-
]),
|
|
31
|
-
getOptimalVideoCodec: codecs.getOptimalVideoCodec,
|
|
32
|
-
isSvcCodec: codecs.isSvcCodec,
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
26
|
describe('Publisher', () => {
|
|
37
27
|
const sessionId = 'session-id-test';
|
|
38
28
|
let publisher: Publisher;
|
|
@@ -69,154 +59,124 @@ describe('Publisher', () => {
|
|
|
69
59
|
sfuClient,
|
|
70
60
|
dispatcher,
|
|
71
61
|
state,
|
|
72
|
-
isDtxEnabled: true,
|
|
73
|
-
isRedEnabled: true,
|
|
74
62
|
logTag: 'test',
|
|
63
|
+
publishOptions: [
|
|
64
|
+
{
|
|
65
|
+
id: 1,
|
|
66
|
+
trackType: TrackType.VIDEO,
|
|
67
|
+
bitrate: 1000,
|
|
68
|
+
// @ts-expect-error - incomplete data
|
|
69
|
+
codec: { name: 'vp9' },
|
|
70
|
+
fps: 30,
|
|
71
|
+
maxTemporalLayers: 3,
|
|
72
|
+
maxSpatialLayers: 3,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
afterEach(() => {
|
|
79
79
|
vi.clearAllMocks();
|
|
80
80
|
vi.resetModules();
|
|
81
|
-
|
|
81
|
+
publisher.dispose();
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
isLocalParticipant: true,
|
|
93
|
-
userId: 'test-user-id',
|
|
94
|
-
sessionId: sessionId,
|
|
95
|
-
publishedTracks: [],
|
|
96
|
-
},
|
|
97
|
-
]);
|
|
84
|
+
describe('Publishing', () => {
|
|
85
|
+
it('should throw when publishing ended tracks', async () => {
|
|
86
|
+
const track = new MediaStreamTrack();
|
|
87
|
+
// @ts-ignore readonly field
|
|
88
|
+
track.readyState = 'ended';
|
|
89
|
+
await expect(publisher.publish(track, TrackType.VIDEO)).rejects.toThrow();
|
|
90
|
+
});
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
deviceId: 'test-device-id',
|
|
92
|
+
it('should throw when attempting to publish a track that has no publish options', async () => {
|
|
93
|
+
const track = new MediaStreamTrack();
|
|
94
|
+
await expect(publisher.publish(track, TrackType.AUDIO)).rejects.toThrow();
|
|
103
95
|
});
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// re-publish a new track
|
|
128
|
-
const newMediaStream = new MediaStream();
|
|
129
|
-
const newTrack = new MediaStreamTrack();
|
|
130
|
-
newMediaStream.addTrack(newTrack);
|
|
131
|
-
|
|
132
|
-
vi.spyOn(newTrack, 'getSettings').mockReturnValue({
|
|
133
|
-
width: 1280,
|
|
134
|
-
height: 720,
|
|
135
|
-
deviceId: 'test-device-id-2',
|
|
97
|
+
it('should add a transceiver for new tracks', async () => {
|
|
98
|
+
const track = new MediaStreamTrack();
|
|
99
|
+
const clone = new MediaStreamTrack();
|
|
100
|
+
vi.spyOn(track, 'clone').mockReturnValue(clone);
|
|
101
|
+
|
|
102
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
103
|
+
|
|
104
|
+
expect(track.clone).toHaveBeenCalled();
|
|
105
|
+
expect(publisher['pc'].addTransceiver).toHaveBeenCalledWith(clone, {
|
|
106
|
+
direction: 'sendonly',
|
|
107
|
+
sendEncodings: [
|
|
108
|
+
{
|
|
109
|
+
rid: 'q',
|
|
110
|
+
active: true,
|
|
111
|
+
maxBitrate: 1000,
|
|
112
|
+
height: 720,
|
|
113
|
+
width: 1280,
|
|
114
|
+
maxFramerate: 30,
|
|
115
|
+
scalabilityMode: 'L3T3_KEY',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
136
119
|
});
|
|
137
120
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
expect(newTrack.addEventListener).not.toHaveBeenCalledWith(
|
|
143
|
-
'ended',
|
|
144
|
-
expect.any(Function),
|
|
145
|
-
);
|
|
146
|
-
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(newTrack);
|
|
147
|
-
|
|
148
|
-
// stop publishing
|
|
149
|
-
await publisher.unpublishStream(TrackType.VIDEO, true);
|
|
150
|
-
expect(newTrack.stop).toHaveBeenCalled();
|
|
151
|
-
expect(state.localParticipant?.publishedTracks).not.toContain(
|
|
152
|
-
TrackType.VIDEO,
|
|
153
|
-
);
|
|
154
|
-
});
|
|
121
|
+
it('should update an existing transceiver for a new track', async () => {
|
|
122
|
+
const track = new MediaStreamTrack();
|
|
123
|
+
const clone = new MediaStreamTrack();
|
|
124
|
+
vi.spyOn(track, 'clone').mockReturnValue(clone);
|
|
155
125
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
126
|
+
const transceiver = new RTCRtpTransceiver();
|
|
127
|
+
publisher['transceiverCache'].add(
|
|
128
|
+
publisher['publishOptions'][0],
|
|
129
|
+
transceiver,
|
|
130
|
+
);
|
|
160
131
|
|
|
161
|
-
|
|
162
|
-
// @ts-ignore
|
|
163
|
-
{
|
|
164
|
-
isLocalParticipant: true,
|
|
165
|
-
userId: 'test-user-id',
|
|
166
|
-
sessionId: sessionId,
|
|
167
|
-
publishedTracks: [],
|
|
168
|
-
},
|
|
169
|
-
]);
|
|
132
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
170
133
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
134
|
+
expect(track.clone).toHaveBeenCalled();
|
|
135
|
+
expect(publisher['pc'].addTransceiver).not.toHaveBeenCalled();
|
|
136
|
+
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(clone);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('Event Handling', () => {
|
|
141
|
+
it('handles changePublishQuality events', () => {
|
|
142
|
+
publisher['changePublishQuality'] = vi.fn();
|
|
143
|
+
dispatcher.dispatch(
|
|
144
|
+
SfuEvent.create({
|
|
145
|
+
eventPayload: {
|
|
146
|
+
oneofKind: 'changePublishQuality',
|
|
147
|
+
changePublishQuality: {
|
|
148
|
+
audioSenders: [],
|
|
149
|
+
videoSenders: [
|
|
150
|
+
{
|
|
151
|
+
publishOptionId: 1,
|
|
152
|
+
trackType: TrackType.VIDEO,
|
|
153
|
+
layers: [],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
publishOptionId: 2,
|
|
157
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
158
|
+
layers: [],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}) as DispatchableMessage<'changePublishQuality'>,
|
|
164
|
+
);
|
|
165
|
+
expect(publisher['changePublishQuality']).toHaveBeenCalled();
|
|
175
166
|
});
|
|
176
167
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(state.localParticipant?.videoStream).toEqual(mediaStream);
|
|
190
|
-
expect(transceiver.setCodecPreferences).toHaveBeenCalled();
|
|
191
|
-
expect(sfuClient.updateMuteState).toHaveBeenCalledWith(
|
|
192
|
-
TrackType.VIDEO,
|
|
193
|
-
false,
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
expect(track.addEventListener).toHaveBeenCalledWith(
|
|
197
|
-
'ended',
|
|
198
|
-
expect.any(Function),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// stop publishing
|
|
202
|
-
await publisher.unpublishStream(TrackType.VIDEO, false);
|
|
203
|
-
expect(track.stop).not.toHaveBeenCalled();
|
|
204
|
-
expect(track.enabled).toBe(false);
|
|
205
|
-
expect(state.localParticipant?.publishedTracks).not.toContain(
|
|
206
|
-
TrackType.VIDEO,
|
|
207
|
-
);
|
|
208
|
-
expect(state.localParticipant?.videoStream).toBeUndefined();
|
|
209
|
-
|
|
210
|
-
const addEventListenerSpy = vi.spyOn(track, 'addEventListener');
|
|
211
|
-
const removeEventListenerSpy = vi.spyOn(track, 'removeEventListener');
|
|
212
|
-
|
|
213
|
-
// start publish again
|
|
214
|
-
await publisher.publishStream(mediaStream, track, TrackType.VIDEO);
|
|
215
|
-
|
|
216
|
-
expect(track.enabled).toBe(true);
|
|
217
|
-
// republishing the same stream should use the previously registered event handlers
|
|
218
|
-
expect(removeEventListenerSpy).not.toHaveBeenCalled();
|
|
219
|
-
expect(addEventListenerSpy).not.toHaveBeenCalled();
|
|
168
|
+
it('handles changePublishOptions events', () => {
|
|
169
|
+
publisher['syncPublishOptions'] = vi.fn();
|
|
170
|
+
dispatcher.dispatch(
|
|
171
|
+
SfuEvent.create({
|
|
172
|
+
eventPayload: {
|
|
173
|
+
oneofKind: 'changePublishOptions',
|
|
174
|
+
changePublishOptions: { publishOptions: [], reason: 'test' },
|
|
175
|
+
},
|
|
176
|
+
}) as DispatchableMessage<'changePublishOptions'>,
|
|
177
|
+
);
|
|
178
|
+
expect(publisher['syncPublishOptions']).toHaveBeenCalled();
|
|
179
|
+
});
|
|
220
180
|
});
|
|
221
181
|
|
|
222
182
|
describe('Publisher ICE Restart', () => {
|
|
@@ -304,34 +264,42 @@ describe('Publisher', () => {
|
|
|
304
264
|
});
|
|
305
265
|
|
|
306
266
|
// inject the transceiver
|
|
307
|
-
publisher['transceiverCache'].
|
|
267
|
+
publisher['transceiverCache'].add(
|
|
268
|
+
// @ts-expect-error incomplete data
|
|
269
|
+
{ trackType: TrackType.VIDEO, id: 1 },
|
|
270
|
+
transceiver,
|
|
271
|
+
);
|
|
308
272
|
|
|
309
|
-
await publisher['changePublishQuality'](
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
273
|
+
await publisher['changePublishQuality']({
|
|
274
|
+
publishOptionId: 1,
|
|
275
|
+
trackType: TrackType.VIDEO,
|
|
276
|
+
layers: [
|
|
277
|
+
{
|
|
278
|
+
name: 'q',
|
|
279
|
+
active: true,
|
|
280
|
+
maxBitrate: 100,
|
|
281
|
+
scaleResolutionDownBy: 4,
|
|
282
|
+
maxFramerate: 30,
|
|
283
|
+
scalabilityMode: '',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'h',
|
|
287
|
+
active: false,
|
|
288
|
+
maxBitrate: 150,
|
|
289
|
+
scaleResolutionDownBy: 2,
|
|
290
|
+
maxFramerate: 30,
|
|
291
|
+
scalabilityMode: '',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'f',
|
|
295
|
+
active: true,
|
|
296
|
+
maxBitrate: 200,
|
|
297
|
+
scaleResolutionDownBy: 1,
|
|
298
|
+
maxFramerate: 30,
|
|
299
|
+
scalabilityMode: '',
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
335
303
|
|
|
336
304
|
expect(getParametersSpy).toHaveBeenCalled();
|
|
337
305
|
expect(setParametersSpy).toHaveBeenCalled();
|
|
@@ -346,9 +314,6 @@ describe('Publisher', () => {
|
|
|
346
314
|
{
|
|
347
315
|
rid: 'h',
|
|
348
316
|
active: false,
|
|
349
|
-
maxBitrate: 150,
|
|
350
|
-
scaleResolutionDownBy: 2,
|
|
351
|
-
maxFramerate: 30,
|
|
352
317
|
},
|
|
353
318
|
{
|
|
354
319
|
rid: 'f',
|
|
@@ -374,18 +339,26 @@ describe('Publisher', () => {
|
|
|
374
339
|
});
|
|
375
340
|
|
|
376
341
|
// inject the transceiver
|
|
377
|
-
publisher['transceiverCache'].
|
|
342
|
+
publisher['transceiverCache'].add(
|
|
343
|
+
// @ts-expect-error incomplete data
|
|
344
|
+
{ trackType: TrackType.VIDEO, id: 1 },
|
|
345
|
+
transceiver,
|
|
346
|
+
);
|
|
378
347
|
|
|
379
|
-
await publisher['changePublishQuality'](
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
348
|
+
await publisher['changePublishQuality']({
|
|
349
|
+
publishOptionId: 1,
|
|
350
|
+
trackType: TrackType.VIDEO,
|
|
351
|
+
layers: [
|
|
352
|
+
{
|
|
353
|
+
name: 'q',
|
|
354
|
+
active: true,
|
|
355
|
+
maxBitrate: 100,
|
|
356
|
+
scaleResolutionDownBy: 4,
|
|
357
|
+
maxFramerate: 30,
|
|
358
|
+
scalabilityMode: '',
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
});
|
|
389
362
|
|
|
390
363
|
expect(getParametersSpy).toHaveBeenCalled();
|
|
391
364
|
expect(setParametersSpy).toHaveBeenCalled();
|
|
@@ -429,18 +402,25 @@ describe('Publisher', () => {
|
|
|
429
402
|
});
|
|
430
403
|
|
|
431
404
|
// inject the transceiver
|
|
432
|
-
publisher['transceiverCache'].
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
405
|
+
publisher['transceiverCache'].add(
|
|
406
|
+
// @ts-expect-error incomplete data
|
|
407
|
+
{ trackType: TrackType.VIDEO, id: 1 },
|
|
408
|
+
transceiver,
|
|
409
|
+
);
|
|
410
|
+
await publisher['changePublishQuality']({
|
|
411
|
+
publishOptionId: 1,
|
|
412
|
+
trackType: TrackType.VIDEO,
|
|
413
|
+
layers: [
|
|
414
|
+
{
|
|
415
|
+
name: 'q',
|
|
416
|
+
active: true,
|
|
417
|
+
maxBitrate: 50,
|
|
418
|
+
scaleResolutionDownBy: 1,
|
|
419
|
+
maxFramerate: 30,
|
|
420
|
+
scalabilityMode: 'L1T3',
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
});
|
|
444
424
|
|
|
445
425
|
expect(getParametersSpy).toHaveBeenCalled();
|
|
446
426
|
expect(setParametersSpy).toHaveBeenCalled();
|
|
@@ -479,18 +459,26 @@ describe('Publisher', () => {
|
|
|
479
459
|
});
|
|
480
460
|
|
|
481
461
|
// inject the transceiver
|
|
482
|
-
publisher['transceiverCache'].
|
|
462
|
+
publisher['transceiverCache'].add(
|
|
463
|
+
// @ts-expect-error incomplete data
|
|
464
|
+
{ trackType: TrackType.VIDEO, id: 1 },
|
|
465
|
+
transceiver,
|
|
466
|
+
);
|
|
483
467
|
|
|
484
|
-
await publisher['changePublishQuality'](
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
468
|
+
await publisher['changePublishQuality']({
|
|
469
|
+
publishOptionId: 1,
|
|
470
|
+
trackType: TrackType.VIDEO,
|
|
471
|
+
layers: [
|
|
472
|
+
{
|
|
473
|
+
name: 'q',
|
|
474
|
+
active: true,
|
|
475
|
+
maxBitrate: 50,
|
|
476
|
+
scaleResolutionDownBy: 1,
|
|
477
|
+
maxFramerate: 30,
|
|
478
|
+
scalabilityMode: 'L1T3',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
});
|
|
494
482
|
|
|
495
483
|
expect(getParametersSpy).toHaveBeenCalled();
|
|
496
484
|
expect(setParametersSpy).toHaveBeenCalled();
|
|
@@ -505,4 +493,213 @@ describe('Publisher', () => {
|
|
|
505
493
|
]);
|
|
506
494
|
});
|
|
507
495
|
});
|
|
496
|
+
|
|
497
|
+
describe('changePublishOptions', () => {
|
|
498
|
+
it('adds missing transceivers', async () => {
|
|
499
|
+
const transceiver = new RTCRtpTransceiver();
|
|
500
|
+
const track = new MediaStreamTrack();
|
|
501
|
+
vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(track);
|
|
502
|
+
vi.spyOn(track, 'clone').mockReturnValue(track);
|
|
503
|
+
// @ts-expect-error private method
|
|
504
|
+
vi.spyOn(publisher, 'addTransceiver');
|
|
505
|
+
|
|
506
|
+
publisher['publishOptions'] = [
|
|
507
|
+
// @ts-expect-error incomplete data
|
|
508
|
+
{ trackType: TrackType.VIDEO, id: 0, codec: { name: 'vp8' } },
|
|
509
|
+
// @ts-expect-error incomplete data
|
|
510
|
+
{ trackType: TrackType.VIDEO, id: 1, codec: { name: 'av1' } },
|
|
511
|
+
// @ts-expect-error incomplete data
|
|
512
|
+
{ trackType: TrackType.VIDEO, id: 2, codec: { name: 'vp9' } },
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
publisher['transceiverCache'].add(
|
|
516
|
+
publisher['publishOptions'][0],
|
|
517
|
+
transceiver,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
vi.spyOn(publisher, 'isPublishing').mockReturnValue(true);
|
|
521
|
+
|
|
522
|
+
// enable av1 and vp9
|
|
523
|
+
await publisher['syncPublishOptions']();
|
|
524
|
+
|
|
525
|
+
expect(publisher['transceiverCache'].items().length).toBe(3);
|
|
526
|
+
expect(publisher['addTransceiver']).toHaveBeenCalledTimes(2);
|
|
527
|
+
expect(publisher['addTransceiver']).toHaveBeenCalledWith(
|
|
528
|
+
track,
|
|
529
|
+
expect.objectContaining({
|
|
530
|
+
trackType: TrackType.VIDEO,
|
|
531
|
+
id: 1,
|
|
532
|
+
codec: { name: 'av1' },
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
expect(publisher['addTransceiver']).toHaveBeenCalledWith(
|
|
536
|
+
track,
|
|
537
|
+
expect.objectContaining({
|
|
538
|
+
trackType: TrackType.VIDEO,
|
|
539
|
+
id: 2,
|
|
540
|
+
codec: { name: 'vp9' },
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('disables extra transceivers', async () => {
|
|
546
|
+
const publishOptions: PublishOption[] = [
|
|
547
|
+
// @ts-expect-error incomplete data
|
|
548
|
+
{ trackType: TrackType.VIDEO, id: 0, codec: { name: 'vp8' } },
|
|
549
|
+
// @ts-expect-error incomplete data
|
|
550
|
+
{ trackType: TrackType.VIDEO, id: 1, codec: { name: 'av1' } },
|
|
551
|
+
// @ts-expect-error incomplete data
|
|
552
|
+
{ trackType: TrackType.VIDEO, id: 2, codec: { name: 'vp9' } },
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
const track = new MediaStreamTrack();
|
|
556
|
+
const transceiver = new RTCRtpTransceiver();
|
|
557
|
+
// @ts-ignore test setup
|
|
558
|
+
transceiver.sender.track = track;
|
|
559
|
+
|
|
560
|
+
publisher['transceiverCache'].add(publishOptions[0], transceiver);
|
|
561
|
+
publisher['transceiverCache'].add(publishOptions[1], transceiver);
|
|
562
|
+
publisher['transceiverCache'].add(publishOptions[2], transceiver);
|
|
563
|
+
|
|
564
|
+
vi.spyOn(publisher, 'isPublishing').mockReturnValue(true);
|
|
565
|
+
// disable av1
|
|
566
|
+
publisher['publishOptions'] = publishOptions.filter(
|
|
567
|
+
(o) => o.codec?.name !== 'av1',
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
await publisher['syncPublishOptions']();
|
|
571
|
+
|
|
572
|
+
expect(publisher['transceiverCache'].items().length).toBe(3);
|
|
573
|
+
expect(track.stop).toHaveBeenCalledOnce();
|
|
574
|
+
expect(transceiver.sender.replaceTrack).toHaveBeenCalledOnce();
|
|
575
|
+
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(null);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
describe('negotiation and track management', () => {
|
|
580
|
+
let cache: TransceiverCache;
|
|
581
|
+
|
|
582
|
+
beforeEach(() => {
|
|
583
|
+
cache = publisher['transceiverCache'];
|
|
584
|
+
const transceiver = new RTCRtpTransceiver();
|
|
585
|
+
const track = new MediaStreamTrack();
|
|
586
|
+
vi.spyOn(track, 'enabled', 'get').mockReturnValue(true);
|
|
587
|
+
vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(track);
|
|
588
|
+
|
|
589
|
+
const inactiveTransceiver = new RTCRtpTransceiver();
|
|
590
|
+
const inactiveTrack = new MediaStreamTrack();
|
|
591
|
+
vi.spyOn(inactiveTrack, 'enabled', 'get').mockReturnValue(false);
|
|
592
|
+
vi.spyOn(inactiveTransceiver.sender, 'track', 'get').mockReturnValue(
|
|
593
|
+
inactiveTrack,
|
|
594
|
+
);
|
|
595
|
+
vi.spyOn(inactiveTrack, 'readyState', 'get').mockReturnValue('ended');
|
|
596
|
+
|
|
597
|
+
// @ts-expect-error incomplete data
|
|
598
|
+
cache.add({ trackType: TrackType.VIDEO, id: 1 }, transceiver);
|
|
599
|
+
// @ts-expect-error incomplete data
|
|
600
|
+
cache.add({ trackType: TrackType.VIDEO, id: 2 }, inactiveTransceiver);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('negotiate should set up the local and remote descriptions', async () => {
|
|
604
|
+
const spyOffer: RTCSessionDescriptionInit = {
|
|
605
|
+
sdp: 'offer-sdp',
|
|
606
|
+
type: 'offer',
|
|
607
|
+
};
|
|
608
|
+
const createOfferSpy = vi
|
|
609
|
+
.spyOn(publisher['pc'], 'createOffer')
|
|
610
|
+
// @ts-expect-error TS picks up the wrong overload
|
|
611
|
+
.mockResolvedValue(spyOffer);
|
|
612
|
+
|
|
613
|
+
const setLocalDescriptionSpy = vi
|
|
614
|
+
.spyOn(publisher['pc'], 'setLocalDescription')
|
|
615
|
+
.mockResolvedValue();
|
|
616
|
+
|
|
617
|
+
const setRemoteDescriptionSpy = vi
|
|
618
|
+
.spyOn(publisher['pc'], 'setRemoteDescription')
|
|
619
|
+
.mockResolvedValue();
|
|
620
|
+
|
|
621
|
+
const addIceCandidateSpy = vi
|
|
622
|
+
.spyOn(publisher['pc'], 'addIceCandidate')
|
|
623
|
+
.mockResolvedValue();
|
|
624
|
+
|
|
625
|
+
sfuClient.setPublisher = vi.fn().mockResolvedValue({
|
|
626
|
+
response: {
|
|
627
|
+
sdp: 'answer-sdp',
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// @ts-expect-error incomplete data
|
|
632
|
+
const trackInfosMock: TrackInfo[] = [{ trackId: '123' }];
|
|
633
|
+
vi.spyOn(publisher, 'getAnnouncedTracks').mockReturnValue(trackInfosMock);
|
|
634
|
+
|
|
635
|
+
sfuClient['iceTrickleBuffer'].push({
|
|
636
|
+
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
637
|
+
iceCandidate: '{ "ufrag": "test", "candidate": "test" }',
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await publisher['negotiate']();
|
|
641
|
+
|
|
642
|
+
expect(sfuClient.setPublisher).toHaveBeenCalledWith({
|
|
643
|
+
sdp: 'offer-sdp',
|
|
644
|
+
tracks: trackInfosMock,
|
|
645
|
+
});
|
|
646
|
+
expect(createOfferSpy).toHaveBeenCalled();
|
|
647
|
+
expect(setLocalDescriptionSpy).toHaveBeenCalledWith(spyOffer);
|
|
648
|
+
expect(setRemoteDescriptionSpy).toHaveBeenCalledWith({
|
|
649
|
+
sdp: 'answer-sdp',
|
|
650
|
+
type: 'answer',
|
|
651
|
+
});
|
|
652
|
+
expect(addIceCandidateSpy).toHaveBeenCalledWith({
|
|
653
|
+
ufrag: 'test',
|
|
654
|
+
candidate: 'test',
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('onNegotiationNeeded delegates to negotiate', () => {
|
|
659
|
+
publisher['negotiate'] = vi.fn().mockResolvedValue(void 0);
|
|
660
|
+
publisher['onNegotiationNeeded']();
|
|
661
|
+
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('getPublishedTracks returns the published tracks', () => {
|
|
665
|
+
const tracks = publisher.getPublishedTracks();
|
|
666
|
+
expect(tracks).toHaveLength(1);
|
|
667
|
+
expect(tracks[0].readyState).toBe('live');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('getAnnouncedTracks should return all tracks', () => {
|
|
671
|
+
const trackInfos = publisher.getAnnouncedTracks('');
|
|
672
|
+
expect(trackInfos).toHaveLength(2);
|
|
673
|
+
expect(trackInfos[0].muted).toBe(false);
|
|
674
|
+
expect(trackInfos[0].mid).toBe('0');
|
|
675
|
+
expect(trackInfos[1].muted).toBe(true);
|
|
676
|
+
expect(trackInfos[1].mid).toBe('1');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('getAnnouncedTracksForReconnect should return only the active tracks', () => {
|
|
680
|
+
const trackInfos = publisher.getAnnouncedTracksForReconnect();
|
|
681
|
+
expect(trackInfos).toHaveLength(1);
|
|
682
|
+
expect(trackInfos[0].muted).toBe(false);
|
|
683
|
+
expect(trackInfos[0].mid).toBe('0');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('isPublishing should return true if there are active tracks', () => {
|
|
687
|
+
expect(publisher.isPublishing(TrackType.VIDEO)).toBe(true);
|
|
688
|
+
expect(publisher.isPublishing(TrackType.SCREEN_SHARE_AUDIO)).toBe(false);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('getTrackType should return the track type', () => {
|
|
692
|
+
expect(
|
|
693
|
+
publisher.getTrackType(cache['cache'][0].transceiver.sender.track!.id),
|
|
694
|
+
).toBe(TrackType.VIDEO);
|
|
695
|
+
expect(publisher.getTrackType('unknown')).toBeUndefined();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('stopTracks should stop tracks', () => {
|
|
699
|
+
const track = cache['cache'][0].transceiver.sender.track;
|
|
700
|
+
vi.spyOn(track, 'stop');
|
|
701
|
+
publisher.stopTracks(TrackType.VIDEO);
|
|
702
|
+
expect(track!.stop).toHaveBeenCalled();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
508
705
|
});
|