@stream-io/video-client 1.14.0 → 1.15.1
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 +1533 -1783
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1514 -1783
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1533 -1783
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +43 -28
- 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/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/types.d.ts +12 -22
- package/package.json +1 -1
- package/src/Call.ts +254 -268
- 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 +38 -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/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/types.ts +12 -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
|
@@ -14,6 +14,7 @@ import { getVideoStream } from '../devices';
|
|
|
14
14
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
15
15
|
import { CameraManager } from '../CameraManager';
|
|
16
16
|
import { of } from 'rxjs';
|
|
17
|
+
import { PermissionsContext } from '../../permissions';
|
|
17
18
|
|
|
18
19
|
vi.mock('../devices.ts', () => {
|
|
19
20
|
console.log('MOCKING devices API');
|
|
@@ -36,7 +37,7 @@ vi.mock('../../Call.ts', () => {
|
|
|
36
37
|
};
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
vi.mock('../../compatibility.ts', () => {
|
|
40
|
+
vi.mock('../../helpers/compatibility.ts', () => {
|
|
40
41
|
console.log('MOCKING mobile device');
|
|
41
42
|
return {
|
|
42
43
|
isMobile: () => true,
|
|
@@ -52,16 +53,16 @@ vi.mock('../../helpers/platforms', () => {
|
|
|
52
53
|
|
|
53
54
|
describe('CameraManager', () => {
|
|
54
55
|
let manager: CameraManager;
|
|
56
|
+
let call: Call;
|
|
55
57
|
|
|
56
58
|
beforeEach(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
59
|
+
call = new Call({
|
|
60
|
+
id: '',
|
|
61
|
+
type: '',
|
|
62
|
+
streamClient: new StreamClient('abc123'),
|
|
63
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
64
|
+
});
|
|
65
|
+
manager = new CameraManager(call);
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
it('list devices', () => {
|
|
@@ -94,8 +95,9 @@ describe('CameraManager', () => {
|
|
|
94
95
|
|
|
95
96
|
await manager.enable();
|
|
96
97
|
|
|
97
|
-
expect(manager['call'].
|
|
98
|
+
expect(manager['call'].publish).toHaveBeenCalledWith(
|
|
98
99
|
manager.state.mediaStream,
|
|
100
|
+
TrackType.VIDEO,
|
|
99
101
|
);
|
|
100
102
|
});
|
|
101
103
|
|
|
@@ -105,10 +107,7 @@ describe('CameraManager', () => {
|
|
|
105
107
|
|
|
106
108
|
await manager.disable();
|
|
107
109
|
|
|
108
|
-
expect(manager['call'].stopPublish).toHaveBeenCalledWith(
|
|
109
|
-
TrackType.VIDEO,
|
|
110
|
-
true,
|
|
111
|
-
);
|
|
110
|
+
expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.VIDEO);
|
|
112
111
|
});
|
|
113
112
|
|
|
114
113
|
it('flip', async () => {
|
|
@@ -205,6 +204,104 @@ describe('CameraManager', () => {
|
|
|
205
204
|
expect(getVideoStream).toHaveBeenCalledOnce();
|
|
206
205
|
});
|
|
207
206
|
|
|
207
|
+
describe('Video Settings', () => {
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
// @ts-expect-error - read only property
|
|
210
|
+
call.permissionsContext = new PermissionsContext();
|
|
211
|
+
call.permissionsContext.hasPermission = vi.fn().mockReturnValue(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should enable the camera when set on the dashboard', async () => {
|
|
215
|
+
vi.spyOn(manager, 'enable');
|
|
216
|
+
await manager.apply(
|
|
217
|
+
// @ts-expect-error - partial settings
|
|
218
|
+
{
|
|
219
|
+
target_resolution: { width: 640, height: 480 },
|
|
220
|
+
camera_facing: 'front',
|
|
221
|
+
camera_default_on: true,
|
|
222
|
+
},
|
|
223
|
+
true,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(manager.state.direction).toBe('front');
|
|
227
|
+
expect(manager.state.status).toBe('enabled');
|
|
228
|
+
expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
|
|
229
|
+
expect(manager.enable).toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not enable the camera when set on the dashboard', async () => {
|
|
233
|
+
vi.spyOn(manager, 'enable');
|
|
234
|
+
await manager.apply(
|
|
235
|
+
// @ts-expect-error - partial settings
|
|
236
|
+
{
|
|
237
|
+
target_resolution: { width: 640, height: 480 },
|
|
238
|
+
camera_facing: 'front',
|
|
239
|
+
camera_default_on: false,
|
|
240
|
+
},
|
|
241
|
+
true,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(manager.state.direction).toBe('front');
|
|
245
|
+
expect(manager.state.status).toBe(undefined);
|
|
246
|
+
expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
|
|
247
|
+
expect(manager.enable).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should not turn on the camera when publish is false', async () => {
|
|
251
|
+
vi.spyOn(manager, 'enable');
|
|
252
|
+
await manager.apply(
|
|
253
|
+
// @ts-expect-error - partial settings
|
|
254
|
+
{
|
|
255
|
+
target_resolution: { width: 640, height: 480 },
|
|
256
|
+
camera_facing: 'front',
|
|
257
|
+
camera_default_on: true,
|
|
258
|
+
},
|
|
259
|
+
false,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(manager.state.direction).toBe('front');
|
|
263
|
+
expect(manager.state.status).toBe(undefined);
|
|
264
|
+
expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
|
|
265
|
+
expect(manager.enable).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should not enable the camera when the user does not have permission', async () => {
|
|
269
|
+
call.permissionsContext.hasPermission = vi.fn().mockReturnValue(false);
|
|
270
|
+
vi.spyOn(manager, 'enable');
|
|
271
|
+
await manager.apply(
|
|
272
|
+
// @ts-expect-error - partial settings
|
|
273
|
+
{
|
|
274
|
+
target_resolution: { width: 640, height: 480 },
|
|
275
|
+
camera_facing: 'front',
|
|
276
|
+
camera_default_on: true,
|
|
277
|
+
},
|
|
278
|
+
true,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(manager.state.direction).toBe(undefined);
|
|
282
|
+
expect(manager.state.status).toBe(undefined);
|
|
283
|
+
expect(manager['targetResolution']).toEqual({ width: 1280, height: 720 });
|
|
284
|
+
expect(manager.enable).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should publish the stream when the camera is already enabled', async () => {
|
|
288
|
+
await manager.enable();
|
|
289
|
+
// @ts-expect-error - private api
|
|
290
|
+
vi.spyOn(manager, 'publishStream');
|
|
291
|
+
await manager.apply(
|
|
292
|
+
// @ts-expect-error - partial settings
|
|
293
|
+
{
|
|
294
|
+
target_resolution: { width: 640, height: 480 },
|
|
295
|
+
camera_facing: 'front',
|
|
296
|
+
camera_default_on: true,
|
|
297
|
+
},
|
|
298
|
+
true,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
expect(manager['publishStream']).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
208
305
|
afterEach(() => {
|
|
209
306
|
vi.clearAllMocks();
|
|
210
307
|
vi.resetModules();
|
|
@@ -4,11 +4,11 @@ import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import {
|
|
7
|
-
MockTrack,
|
|
8
7
|
emitDeviceIds,
|
|
9
8
|
mockBrowserPermission,
|
|
10
9
|
mockCall,
|
|
11
10
|
mockDeviceIds$,
|
|
11
|
+
MockTrack,
|
|
12
12
|
mockVideoDevices,
|
|
13
13
|
mockVideoStream,
|
|
14
14
|
} from './mocks';
|
|
@@ -123,7 +123,7 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
123
123
|
|
|
124
124
|
await manager.disable();
|
|
125
125
|
|
|
126
|
-
expect(manager.stopPublishStream).
|
|
126
|
+
expect(manager.stopPublishStream).toHaveBeenCalled();
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
it('disable device with forceStop', async () => {
|
|
@@ -134,7 +134,7 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
134
134
|
|
|
135
135
|
await manager.disable(true);
|
|
136
136
|
|
|
137
|
-
expect(manager.stopPublishStream).
|
|
137
|
+
expect(manager.stopPublishStream).toHaveBeenCalled();
|
|
138
138
|
expect(manager.state.mediaStream).toBeUndefined();
|
|
139
139
|
expect(manager.state.status).toBe('disabled');
|
|
140
140
|
});
|
|
@@ -179,7 +179,7 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
179
179
|
const deviceId = mockVideoDevices[1].deviceId;
|
|
180
180
|
await manager.select(deviceId);
|
|
181
181
|
|
|
182
|
-
expect(manager.stopPublishStream).
|
|
182
|
+
expect(manager.stopPublishStream).toHaveBeenCalled();
|
|
183
183
|
expect(manager.getStream).toHaveBeenCalledWith({
|
|
184
184
|
deviceId: { exact: deviceId },
|
|
185
185
|
});
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
createSoundDetector,
|
|
24
24
|
SoundStateChangeHandler,
|
|
25
25
|
} from '../../helpers/sound-detector';
|
|
26
|
+
import { PermissionsContext } from '../../permissions';
|
|
26
27
|
|
|
27
28
|
vi.mock('../devices.ts', () => {
|
|
28
29
|
console.log('MOCKING devices API');
|
|
@@ -60,7 +61,7 @@ class NoiseCancellationStub implements INoiseCancellation {
|
|
|
60
61
|
enable = () => this.listeners['change']?.forEach((l) => l(true));
|
|
61
62
|
disable = () => this.listeners['change']?.forEach((l) => l(false));
|
|
62
63
|
dispose = () => Promise.resolve(undefined);
|
|
63
|
-
toFilter = () =>
|
|
64
|
+
toFilter = () => (ms: MediaStream) => ({ output: ms });
|
|
64
65
|
on = (event, callback) => {
|
|
65
66
|
(this.listeners[event] ??= []).push(callback);
|
|
66
67
|
return () => {};
|
|
@@ -70,17 +71,16 @@ class NoiseCancellationStub implements INoiseCancellation {
|
|
|
70
71
|
|
|
71
72
|
describe('MicrophoneManager', () => {
|
|
72
73
|
let manager: MicrophoneManager;
|
|
74
|
+
let call: Call;
|
|
73
75
|
|
|
74
76
|
beforeEach(() => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'disable-tracks',
|
|
83
|
-
);
|
|
77
|
+
call = new Call({
|
|
78
|
+
id: '',
|
|
79
|
+
type: '',
|
|
80
|
+
streamClient: new StreamClient('abc123'),
|
|
81
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
82
|
+
});
|
|
83
|
+
manager = new MicrophoneManager(call, 'disable-tracks');
|
|
84
84
|
});
|
|
85
85
|
it('list devices', () => {
|
|
86
86
|
const spy = vi.fn();
|
|
@@ -110,8 +110,9 @@ describe('MicrophoneManager', () => {
|
|
|
110
110
|
|
|
111
111
|
await manager.enable();
|
|
112
112
|
|
|
113
|
-
expect(manager['call'].
|
|
113
|
+
expect(manager['call'].publish).toHaveBeenCalledWith(
|
|
114
114
|
manager.state.mediaStream,
|
|
115
|
+
TrackType.AUDIO,
|
|
115
116
|
);
|
|
116
117
|
});
|
|
117
118
|
|
|
@@ -121,10 +122,7 @@ describe('MicrophoneManager', () => {
|
|
|
121
122
|
|
|
122
123
|
await manager.disable();
|
|
123
124
|
|
|
124
|
-
expect(manager['call'].stopPublish).toHaveBeenCalledWith(
|
|
125
|
-
TrackType.AUDIO,
|
|
126
|
-
false,
|
|
127
|
-
);
|
|
125
|
+
expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.AUDIO);
|
|
128
126
|
});
|
|
129
127
|
|
|
130
128
|
it('disable-enable mic should set track.enabled', async () => {
|
|
@@ -244,7 +242,6 @@ describe('MicrophoneManager', () => {
|
|
|
244
242
|
|
|
245
243
|
describe('Noise Cancellation', () => {
|
|
246
244
|
it('should register filter if all preconditions are met', async () => {
|
|
247
|
-
const call = manager['call'];
|
|
248
245
|
call.state.setCallingState(CallingState.IDLE);
|
|
249
246
|
const registerFilter = vi.spyOn(manager, 'registerFilter');
|
|
250
247
|
const noiseCancellation = new NoiseCancellationStub();
|
|
@@ -259,12 +256,10 @@ describe('MicrophoneManager', () => {
|
|
|
259
256
|
const noiseCancellation = new NoiseCancellationStub();
|
|
260
257
|
await manager.enableNoiseCancellation(noiseCancellation);
|
|
261
258
|
await manager.disableNoiseCancellation();
|
|
262
|
-
const call = manager['call'];
|
|
263
259
|
expect(call.notifyNoiseCancellationStopped).toBeCalled();
|
|
264
260
|
});
|
|
265
261
|
|
|
266
262
|
it('should throw when own capabilities are missing', async () => {
|
|
267
|
-
const call = manager['call'];
|
|
268
263
|
call.state.setOwnCapabilities([]);
|
|
269
264
|
|
|
270
265
|
await expect(() =>
|
|
@@ -273,7 +268,6 @@ describe('MicrophoneManager', () => {
|
|
|
273
268
|
});
|
|
274
269
|
|
|
275
270
|
it('should throw when noise cancellation is disabled in call settings', async () => {
|
|
276
|
-
const call = manager['call'];
|
|
277
271
|
call.state.setOwnCapabilities([OwnCapability.ENABLE_NOISE_CANCELLATION]);
|
|
278
272
|
call.state.updateFromCallResponse({
|
|
279
273
|
// @ts-expect-error partial data
|
|
@@ -289,7 +283,6 @@ describe('MicrophoneManager', () => {
|
|
|
289
283
|
});
|
|
290
284
|
|
|
291
285
|
it('should automatically enable noise noise suppression after joining a call', async () => {
|
|
292
|
-
const call = manager['call'];
|
|
293
286
|
call.state.setCallingState(CallingState.IDLE); // reset state
|
|
294
287
|
call.state.updateFromCallResponse({
|
|
295
288
|
settings: {
|
|
@@ -320,7 +313,6 @@ describe('MicrophoneManager', () => {
|
|
|
320
313
|
});
|
|
321
314
|
|
|
322
315
|
it('should automatically disable noise suppression after leaving the call', async () => {
|
|
323
|
-
const call = manager['call'];
|
|
324
316
|
const noiseCancellation = new NoiseCancellationStub();
|
|
325
317
|
const noiseSuppressionDisable = vi.spyOn(noiseCancellation, 'disable');
|
|
326
318
|
await manager.enableNoiseCancellation(noiseCancellation);
|
|
@@ -339,6 +331,52 @@ describe('MicrophoneManager', () => {
|
|
|
339
331
|
});
|
|
340
332
|
});
|
|
341
333
|
|
|
334
|
+
describe('Audio Settings', () => {
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
// @ts-expect-error - read only property
|
|
337
|
+
call.permissionsContext = new PermissionsContext();
|
|
338
|
+
call.permissionsContext.hasPermission = vi.fn().mockReturnValue(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should turn the mic on when set on dashboard', async () => {
|
|
342
|
+
const enable = vi.spyOn(manager, 'enable');
|
|
343
|
+
// @ts-expect-error - partial data
|
|
344
|
+
await manager.apply({ mic_default_on: true }, true);
|
|
345
|
+
expect(enable).toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should not turn the mic on when set on dashboard', async () => {
|
|
349
|
+
const enable = vi.spyOn(manager, 'enable');
|
|
350
|
+
// @ts-expect-error - partial data
|
|
351
|
+
await manager.apply({ mic_default_on: false }, true);
|
|
352
|
+
expect(enable).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should not turn on the mic when publish is false', async () => {
|
|
356
|
+
const enable = vi.spyOn(manager, 'enable');
|
|
357
|
+
// @ts-expect-error - partial data
|
|
358
|
+
await manager.apply({ mic_default_on: true }, false);
|
|
359
|
+
expect(enable).not.toHaveBeenCalled();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should not turn on the mic when permission is missing', async () => {
|
|
363
|
+
call.permissionsContext.hasPermission = vi.fn().mockReturnValue(false);
|
|
364
|
+
const enable = vi.spyOn(manager, 'enable');
|
|
365
|
+
// @ts-expect-error - partial data
|
|
366
|
+
await manager.apply({ mic_default_on: true }, true);
|
|
367
|
+
expect(enable).not.toHaveBeenCalled();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should publish the audio stream when mic is turned on before settings are applied', async () => {
|
|
371
|
+
await manager.enable();
|
|
372
|
+
// @ts-expect-error - private api
|
|
373
|
+
vi.spyOn(manager, 'publishStream');
|
|
374
|
+
// @ts-expect-error - partial data
|
|
375
|
+
await manager.apply({ mic_default_on: true }, true);
|
|
376
|
+
expect(manager['publishStream']).toHaveBeenCalled();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
342
380
|
afterEach(() => {
|
|
343
381
|
vi.clearAllMocks();
|
|
344
382
|
vi.resetModules();
|
|
@@ -49,8 +49,8 @@ describe('ScreenShareManager', () => {
|
|
|
49
49
|
expect(RxUtils.getCurrentValue(devices)).toEqual([]);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it('select device', () => {
|
|
53
|
-
expect(manager.select('any-device-id')).rejects.toThrowError();
|
|
52
|
+
it('select device', async () => {
|
|
53
|
+
await expect(manager.select('any-device-id')).rejects.toThrowError();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('get stream', async () => {
|
|
@@ -113,8 +113,9 @@ describe('ScreenShareManager', () => {
|
|
|
113
113
|
const call = manager['call'];
|
|
114
114
|
call.state.setCallingState(CallingState.JOINED);
|
|
115
115
|
await manager.enable();
|
|
116
|
-
expect(call.
|
|
116
|
+
expect(call.publish).toHaveBeenCalledWith(
|
|
117
117
|
manager.state.mediaStream,
|
|
118
|
+
TrackType.SCREEN_SHARE,
|
|
118
119
|
);
|
|
119
120
|
});
|
|
120
121
|
|
|
@@ -125,10 +126,9 @@ describe('ScreenShareManager', () => {
|
|
|
125
126
|
|
|
126
127
|
await manager.disable();
|
|
127
128
|
expect(manager.state.status).toEqual('disabled');
|
|
128
|
-
expect(call.stopPublish).toHaveBeenCalledWith(TrackType.SCREEN_SHARE, true);
|
|
129
129
|
expect(call.stopPublish).toHaveBeenCalledWith(
|
|
130
|
+
TrackType.SCREEN_SHARE,
|
|
130
131
|
TrackType.SCREEN_SHARE_AUDIO,
|
|
131
|
-
true,
|
|
132
132
|
);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
@@ -96,6 +96,7 @@ export const mockCall = (): Partial<Call> => {
|
|
|
96
96
|
publishVideoStream: vi.fn(),
|
|
97
97
|
publishAudioStream: vi.fn(),
|
|
98
98
|
publishScreenShareStream: vi.fn(),
|
|
99
|
+
publish: vi.fn(),
|
|
99
100
|
stopPublish: vi.fn(),
|
|
100
101
|
notifyNoiseCancellationStarting: vi.fn().mockResolvedValue(undefined),
|
|
101
102
|
notifyNoiseCancellationStopped: vi.fn().mockResolvedValue(undefined),
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Call } from '../../Call';
|
|
3
|
+
import { Dispatcher } from '../../rtc';
|
|
4
|
+
import { CallState } from '../../store';
|
|
5
|
+
import {
|
|
6
|
+
watchConnectionQualityChanged,
|
|
7
|
+
watchLiveEnded,
|
|
8
|
+
watchParticipantCountChanged,
|
|
9
|
+
watchPinsUpdated,
|
|
10
|
+
} from '../internal';
|
|
11
|
+
import {
|
|
12
|
+
ConnectionQuality,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
} from '../../gen/video/sfu/models/models';
|
|
15
|
+
|
|
16
|
+
describe('internal events', () => {
|
|
17
|
+
it('handles connectionQualityChanged', () => {
|
|
18
|
+
const state = new CallState();
|
|
19
|
+
const dispatcher = new Dispatcher();
|
|
20
|
+
state.setParticipants([
|
|
21
|
+
// @ts-expect-error
|
|
22
|
+
{ sessionId: 'session-1', connectionQuality: ConnectionQuality.POOR },
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
watchConnectionQualityChanged(dispatcher, state);
|
|
26
|
+
|
|
27
|
+
dispatcher.dispatch({
|
|
28
|
+
eventPayload: {
|
|
29
|
+
oneofKind: 'connectionQualityChanged',
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
connectionQualityChanged: {
|
|
32
|
+
connectionQualityUpdates: [
|
|
33
|
+
{
|
|
34
|
+
sessionId: 'session-1',
|
|
35
|
+
connectionQuality: ConnectionQuality.EXCELLENT,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
expect(state.participants).toEqual([
|
|
42
|
+
{
|
|
43
|
+
sessionId: 'session-1',
|
|
44
|
+
connectionQuality: ConnectionQuality.EXCELLENT,
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles healthCheckResponse', () => {
|
|
50
|
+
const state = new CallState();
|
|
51
|
+
const dispatcher = new Dispatcher();
|
|
52
|
+
state.setParticipantCount(0);
|
|
53
|
+
state.setAnonymousParticipantCount(0);
|
|
54
|
+
|
|
55
|
+
watchParticipantCountChanged(dispatcher, state);
|
|
56
|
+
|
|
57
|
+
dispatcher.dispatch({
|
|
58
|
+
eventPayload: {
|
|
59
|
+
oneofKind: 'healthCheckResponse',
|
|
60
|
+
// @ts-expect-error
|
|
61
|
+
healthCheckResponse: {
|
|
62
|
+
participantCount: {
|
|
63
|
+
total: 5,
|
|
64
|
+
anonymous: 2,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
expect(state.participantCount).toBe(5);
|
|
70
|
+
expect(state.anonymousParticipantCount).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles liveEnded', () => {
|
|
74
|
+
const dispatcher = new Dispatcher();
|
|
75
|
+
const call = {
|
|
76
|
+
permissionsContext: { hasPermission: () => false },
|
|
77
|
+
leave: vi.fn().mockResolvedValue(undefined),
|
|
78
|
+
logger: vi.fn(),
|
|
79
|
+
} as unknown as Call;
|
|
80
|
+
|
|
81
|
+
watchLiveEnded(dispatcher, call);
|
|
82
|
+
|
|
83
|
+
dispatcher.dispatch({
|
|
84
|
+
eventPayload: {
|
|
85
|
+
oneofKind: 'error',
|
|
86
|
+
// @ts-expect-error
|
|
87
|
+
error: { code: ErrorCode.LIVE_ENDED },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
expect(call.leave).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles liveEnded when user has permission to stay in backstage', () => {
|
|
94
|
+
const dispatcher = new Dispatcher();
|
|
95
|
+
const call = {
|
|
96
|
+
permissionsContext: { hasPermission: () => true },
|
|
97
|
+
leave: vi.fn().mockResolvedValue(undefined),
|
|
98
|
+
logger: vi.fn(),
|
|
99
|
+
} as unknown as Call;
|
|
100
|
+
|
|
101
|
+
watchLiveEnded(dispatcher, call);
|
|
102
|
+
|
|
103
|
+
dispatcher.dispatch({
|
|
104
|
+
eventPayload: {
|
|
105
|
+
oneofKind: 'error',
|
|
106
|
+
// @ts-expect-error
|
|
107
|
+
error: { code: ErrorCode.LIVE_ENDED },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
expect(call.leave).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles pinUpdated', () => {
|
|
114
|
+
const state = new CallState();
|
|
115
|
+
state.setParticipants([
|
|
116
|
+
// @ts-expect-error
|
|
117
|
+
{ userId: 'u1', sessionId: 'session-1', pin: { isLocalPin: false } },
|
|
118
|
+
// @ts-expect-error
|
|
119
|
+
{ userId: 'u2', sessionId: 'session-2', pin: { isLocalPin: false } },
|
|
120
|
+
]);
|
|
121
|
+
const update = watchPinsUpdated(state);
|
|
122
|
+
update({ pins: [{ userId: 'u1', sessionId: 'session-1' }] });
|
|
123
|
+
expect(state.participants).toEqual([
|
|
124
|
+
{
|
|
125
|
+
userId: 'u1',
|
|
126
|
+
sessionId: 'session-1',
|
|
127
|
+
pin: { isLocalPin: false, pinnedAt: expect.any(Number) },
|
|
128
|
+
},
|
|
129
|
+
{ userId: 'u2', sessionId: 'session-2', pin: undefined },
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -20,12 +20,9 @@ describe('mutes', () => {
|
|
|
20
20
|
id: 'test',
|
|
21
21
|
streamClient: new StreamClient('api-key'),
|
|
22
22
|
});
|
|
23
|
-
// disable all event handlers
|
|
24
|
-
call['dispatcher'].offAll();
|
|
25
23
|
|
|
26
24
|
// @ts-expect-error partial data
|
|
27
25
|
call.publisher = vi.fn();
|
|
28
|
-
// @ts-expect-error partial data
|
|
29
26
|
call.publisher.isPublishing = vi.fn().mockReturnValue(true);
|
|
30
27
|
|
|
31
28
|
vi.spyOn(call.camera, 'disable').mockResolvedValue(undefined);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CallState } from '../../store';
|
|
3
|
+
import { noopComparator } from '../../sorting';
|
|
4
|
+
import {
|
|
5
|
+
watchAudioLevelChanged,
|
|
6
|
+
watchDominantSpeakerChanged,
|
|
7
|
+
} from '../speaker';
|
|
8
|
+
import { Dispatcher } from '../../rtc';
|
|
9
|
+
|
|
10
|
+
describe('speaker events', () => {
|
|
11
|
+
it('should watch dominant speaker changed', () => {
|
|
12
|
+
const state = new CallState();
|
|
13
|
+
state.setSortParticipantsBy(noopComparator());
|
|
14
|
+
state.setParticipants([
|
|
15
|
+
// @ts-expect-error
|
|
16
|
+
{ userId: 'user-1', sessionId: 'session-1', isDominantSpeaker: false },
|
|
17
|
+
// @ts-expect-error
|
|
18
|
+
{ userId: 'user-2', sessionId: 'session-2', isDominantSpeaker: true },
|
|
19
|
+
]);
|
|
20
|
+
const dispatcher = new Dispatcher();
|
|
21
|
+
|
|
22
|
+
watchDominantSpeakerChanged(dispatcher, state);
|
|
23
|
+
|
|
24
|
+
dispatcher.dispatch({
|
|
25
|
+
eventPayload: {
|
|
26
|
+
oneofKind: 'dominantSpeakerChanged',
|
|
27
|
+
// @ts-expect-error
|
|
28
|
+
dominantSpeakerChanged: {
|
|
29
|
+
userId: 'user-1',
|
|
30
|
+
sessionId: 'session-1',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(state.participants).toEqual([
|
|
36
|
+
{ userId: 'user-1', sessionId: 'session-1', isDominantSpeaker: true },
|
|
37
|
+
{ userId: 'user-2', sessionId: 'session-2', isDominantSpeaker: false },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('watchAudioLevelChanged', () => {
|
|
42
|
+
const state = new CallState();
|
|
43
|
+
state.setSortParticipantsBy(noopComparator());
|
|
44
|
+
state.setParticipants([
|
|
45
|
+
// @ts-expect-error
|
|
46
|
+
{
|
|
47
|
+
userId: 'user-1',
|
|
48
|
+
sessionId: 'session-1',
|
|
49
|
+
audioLevel: 0,
|
|
50
|
+
isSpeaking: false,
|
|
51
|
+
},
|
|
52
|
+
// @ts-expect-error
|
|
53
|
+
{
|
|
54
|
+
userId: 'user-2',
|
|
55
|
+
sessionId: 'session-2',
|
|
56
|
+
audioLevel: 0,
|
|
57
|
+
isSpeaking: false,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
const dispatcher = new Dispatcher();
|
|
61
|
+
|
|
62
|
+
watchAudioLevelChanged(dispatcher, state);
|
|
63
|
+
|
|
64
|
+
dispatcher.dispatch({
|
|
65
|
+
eventPayload: {
|
|
66
|
+
oneofKind: 'audioLevelChanged',
|
|
67
|
+
// @ts-expect-error
|
|
68
|
+
audioLevelChanged: {
|
|
69
|
+
audioLevels: [
|
|
70
|
+
{ sessionId: 'session-1', level: 0.5, isSpeaking: true },
|
|
71
|
+
{ sessionId: 'session-2', level: 0.5, isSpeaking: true },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(state.participants).toEqual([
|
|
78
|
+
{
|
|
79
|
+
userId: 'user-1',
|
|
80
|
+
sessionId: 'session-1',
|
|
81
|
+
audioLevel: 0.5,
|
|
82
|
+
isSpeaking: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
userId: 'user-2',
|
|
86
|
+
sessionId: 'session-2',
|
|
87
|
+
audioLevel: 0.5,
|
|
88
|
+
isSpeaking: true,
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
VisibilityState,
|
|
13
13
|
} from '../types';
|
|
14
14
|
import { CallState } from '../store';
|
|
15
|
-
import { trackTypeToParticipantStreamKey } from '../rtc
|
|
15
|
+
import { trackTypeToParticipantStreamKey } from '../rtc';
|
|
16
|
+
import { pushToIfMissing } from '../helpers/array';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* An event responder which handles the `participantJoined` event.
|
|
@@ -88,7 +89,7 @@ export const watchTrackPublished = (state: CallState) => {
|
|
|
88
89
|
state.updateOrAddParticipant(sessionId, participant);
|
|
89
90
|
} else {
|
|
90
91
|
state.updateParticipant(sessionId, (p) => ({
|
|
91
|
-
publishedTracks: [...p.publishedTracks, type
|
|
92
|
+
publishedTracks: pushToIfMissing([...p.publishedTracks], type),
|
|
92
93
|
}));
|
|
93
94
|
}
|
|
94
95
|
};
|
|
@@ -114,8 +115,6 @@ export const watchTrackUnpublished = (state: CallState) => {
|
|
|
114
115
|
};
|
|
115
116
|
};
|
|
116
117
|
|
|
117
|
-
const unique = <T>(v: T, i: number, arr: T[]) => arr.indexOf(v) === i;
|
|
118
|
-
|
|
119
118
|
/**
|
|
120
119
|
* Reconciles orphaned tracks (if any) for the given participant.
|
|
121
120
|
*
|