@stream-io/video-client 0.4.4 → 0.4.6

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.
@@ -3,7 +3,12 @@ import { StreamClient } from '../../coordinator/connection/client';
3
3
  import { CallingState, StreamVideoWriteableStateStore } from '../../store';
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
6
- import { mockAudioDevices, mockAudioStream, mockCall } from './mocks';
6
+ import {
7
+ mockAudioDevices,
8
+ mockAudioStream,
9
+ mockCall,
10
+ mockDeviceIds$,
11
+ } from './mocks';
7
12
  import { getAudioStream } from '../devices';
8
13
  import { TrackType } from '../../gen/video/sfu/models/models';
9
14
  import { MicrophoneManager } from '../MicrophoneManager';
@@ -22,6 +27,7 @@ vi.mock('../devices.ts', () => {
22
27
  return of(mockAudioDevices);
23
28
  }),
24
29
  getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
30
+ deviceIds$: mockDeviceIds$(),
25
31
  };
26
32
  });
27
33
 
@@ -4,7 +4,7 @@ import { Call } from '../../Call';
4
4
  import { StreamClient } from '../../coordinator/connection/client';
5
5
  import { CallingState, StreamVideoWriteableStateStore } from '../../store';
6
6
  import * as RxUtils from '../../store/rxUtils';
7
- import { mockCall, mockScreenShareStream } from './mocks';
7
+ import { mockCall, mockDeviceIds$, mockScreenShareStream } from './mocks';
8
8
  import { getScreenShareStream } from '../devices';
9
9
  import { TrackType } from '../../gen/video/sfu/models/models';
10
10
 
@@ -14,6 +14,7 @@ vi.mock('../devices.ts', () => {
14
14
  disposeOfMediaStream: vi.fn(),
15
15
  getScreenShareStream: vi.fn(() => Promise.resolve(mockScreenShareStream())),
16
16
  checkIfAudioOutputChangeSupported: vi.fn(() => Promise.resolve(true)),
17
+ deviceIds$: () => mockDeviceIds$(),
17
18
  };
18
19
  });
19
20
 
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
2
- import { mockAudioDevices } from './mocks';
2
+ import { emitDeviceIds, mockAudioDevices, mockDeviceIds$ } from './mocks';
3
3
  import { of } from 'rxjs';
4
4
  import { SpeakerManager } from '../SpeakerManager';
5
5
  import { checkIfAudioOutputChangeSupported } from '../devices';
@@ -9,6 +9,7 @@ vi.mock('../devices.ts', () => {
9
9
  return {
10
10
  getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
11
11
  checkIfAudioOutputChangeSupported: vi.fn(() => true),
12
+ deviceIds$: mockDeviceIds$(),
12
13
  };
13
14
  });
14
15
 
@@ -59,6 +60,16 @@ describe('SpeakerManager.test', () => {
59
60
  expect(manager.state.volume).toBe(0.5);
60
61
  });
61
62
 
63
+ it('should disable device if selected device is disconnected', () => {
64
+ emitDeviceIds(mockAudioDevices);
65
+ const deviceId = mockAudioDevices[1].deviceId;
66
+ manager.select(deviceId);
67
+
68
+ emitDeviceIds(mockAudioDevices.slice(2));
69
+
70
+ expect(manager.state.selectedDevice).toBe('');
71
+ });
72
+
62
73
  afterEach(() => {
63
74
  vi.clearAllMocks();
64
75
  vi.resetModules();
@@ -2,6 +2,7 @@ import { vi } from 'vitest';
2
2
  import { CallingState, CallState } from '../../store';
3
3
  import { OwnCapability } from '../../gen/coordinator';
4
4
  import { Call } from '../../Call';
5
+ import { Subject } from 'rxjs';
5
6
 
6
7
  export const mockVideoDevices = [
7
8
  {
@@ -80,8 +81,14 @@ export const mockCall = (): Partial<Call> => {
80
81
  };
81
82
  };
82
83
 
84
+ export type MockTrack = Partial<MediaStreamTrack> & {
85
+ eventHandlers: { [key: string]: EventListenerOrEventListenerObject };
86
+ readyState: string;
87
+ };
88
+
83
89
  export const mockAudioStream = () => {
84
- const track = {
90
+ const track: MockTrack = {
91
+ eventHandlers: {},
85
92
  getSettings: () => ({
86
93
  deviceId: mockAudioDevices[0].deviceId,
87
94
  }),
@@ -90,15 +97,22 @@ export const mockAudioStream = () => {
90
97
  stop: () => {
91
98
  track.readyState = 'ended';
92
99
  },
100
+ addEventListener: (
101
+ event: string,
102
+ handler: EventListenerOrEventListenerObject,
103
+ ) => {
104
+ track.eventHandlers[event] = handler;
105
+ },
93
106
  };
94
107
  return {
95
108
  getTracks: () => [track],
96
109
  getAudioTracks: () => [track],
97
- } as MediaStream;
110
+ } as any as MediaStream;
98
111
  };
99
112
 
100
113
  export const mockVideoStream = () => {
101
- const track = {
114
+ const track: MockTrack = {
115
+ eventHandlers: {},
102
116
  getSettings: () => ({
103
117
  deviceId: mockVideoDevices[0].deviceId,
104
118
  width: 1280,
@@ -109,15 +123,22 @@ export const mockVideoStream = () => {
109
123
  stop: () => {
110
124
  track.readyState = 'ended';
111
125
  },
126
+ addEventListener: (
127
+ event: string,
128
+ handler: EventListenerOrEventListenerObject,
129
+ ) => {
130
+ track.eventHandlers[event] = handler;
131
+ },
112
132
  };
113
133
  return {
114
134
  getTracks: () => [track],
115
135
  getVideoTracks: () => [track],
116
- } as MediaStream;
136
+ } as any as MediaStream;
117
137
  };
118
138
 
119
139
  export const mockScreenShareStream = (includeAudio: boolean = true) => {
120
140
  const track = {
141
+ eventHandlers: {},
121
142
  getSettings: () => ({
122
143
  deviceId: 'screen',
123
144
  }),
@@ -126,11 +147,18 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
126
147
  stop: () => {
127
148
  track.readyState = 'ended';
128
149
  },
150
+ addEventListener: (
151
+ event: string,
152
+ handler: EventListenerOrEventListenerObject,
153
+ ) => {
154
+ track.eventHandlers[event] = handler;
155
+ },
129
156
  };
130
157
 
131
158
  const tracks = [track];
132
159
  if (includeAudio) {
133
- tracks.push({
160
+ const audioTrack = {
161
+ eventHandlers: {},
134
162
  getSettings: () => ({
135
163
  deviceId: 'screen-audio',
136
164
  }),
@@ -139,12 +167,33 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
139
167
  stop: () => {
140
168
  track.readyState = 'ended';
141
169
  },
142
- });
170
+ addEventListener: (
171
+ event: string,
172
+ handler: EventListenerOrEventListenerObject,
173
+ ) => {
174
+ audioTrack.eventHandlers[event] = handler;
175
+ },
176
+ };
177
+ tracks.push(audioTrack);
143
178
  }
144
179
 
145
180
  return {
146
181
  getTracks: () => tracks,
147
182
  getVideoTracks: () => tracks,
148
183
  getAudioTracks: () => tracks,
149
- } as MediaStream;
184
+ } as any as MediaStream;
185
+ };
186
+
187
+ let deviceIds: Subject<MediaDeviceInfo[]>;
188
+ export const mockDeviceIds$ = () => {
189
+ global.navigator = {
190
+ //@ts-expect-error
191
+ mediaDevices: {},
192
+ };
193
+ deviceIds = new Subject();
194
+ return deviceIds;
195
+ };
196
+
197
+ export const emitDeviceIds = (values: MediaDeviceInfo[]) => {
198
+ deviceIds.next(values);
150
199
  };
@@ -1,13 +1,10 @@
1
1
  import {
2
- combineLatest,
3
2
  concatMap,
4
3
  debounceTime,
5
- filter,
6
4
  from,
7
5
  map,
8
6
  merge,
9
7
  Observable,
10
- pairwise,
11
8
  shareReplay,
12
9
  } from 'rxjs';
13
10
  import { getLogger } from '../logger';
@@ -61,8 +58,7 @@ const getDevices = (
61
58
  /**
62
59
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
63
60
  *
64
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
65
- */
61
+ * */
66
62
  export const checkIfAudioOutputChangeSupported = () => {
67
63
  if (typeof document === 'undefined') return false;
68
64
  const element = document.createElement('audio');
@@ -258,124 +254,16 @@ export const getScreenShareStream = async (
258
254
  }
259
255
  };
260
256
 
261
- const watchForDisconnectedDevice = (
262
- kind: MediaDeviceKind,
263
- deviceId$: Observable<string | undefined>,
264
- ) => {
265
- let devices$;
266
- switch (kind) {
267
- case 'audioinput':
268
- devices$ = getAudioDevices();
269
- break;
270
- case 'videoinput':
271
- devices$ = getVideoDevices();
272
- break;
273
- case 'audiooutput':
274
- devices$ = getAudioOutputDevices();
275
- break;
276
- }
277
- return combineLatest([devices$, deviceId$]).pipe(
278
- filter(
279
- ([devices, deviceId]) =>
280
- !!deviceId && !devices.find((d) => d.deviceId === deviceId),
281
- ),
282
- map(() => true),
283
- );
284
- };
285
-
286
- /**
287
- * Notifies the subscriber if a given 'audioinput' device is disconnected
288
- *
289
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
290
- * @param deviceId$ an Observable that specifies which device to watch for
291
- * @returns
292
- */
293
- export const watchForDisconnectedAudioDevice = (
294
- deviceId$: Observable<string | undefined>,
295
- ) => {
296
- return watchForDisconnectedDevice('audioinput', deviceId$);
297
- };
298
-
299
- /**
300
- * Notifies the subscriber if a given 'videoinput' device is disconnected
301
- *
302
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
303
- * @param deviceId$ an Observable that specifies which device to watch for
304
- * @returns
305
- */
306
- export const watchForDisconnectedVideoDevice = (
307
- deviceId$: Observable<string | undefined>,
308
- ) => {
309
- return watchForDisconnectedDevice('videoinput', deviceId$);
310
- };
311
-
312
- /**
313
- * Notifies the subscriber if a given 'audiooutput' device is disconnected
314
- *
315
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
316
- * @param deviceId$ an Observable that specifies which device to watch for
317
- * @returns
318
- */
319
- export const watchForDisconnectedAudioOutputDevice = (
320
- deviceId$: Observable<string | undefined>,
321
- ) => {
322
- return watchForDisconnectedDevice('audiooutput', deviceId$);
323
- };
324
-
325
- const watchForAddedDefaultDevice = (kind: MediaDeviceKind) => {
326
- let devices$;
327
- switch (kind) {
328
- case 'audioinput':
329
- devices$ = getAudioDevices();
330
- break;
331
- case 'videoinput':
332
- devices$ = getVideoDevices();
333
- break;
334
- case 'audiooutput':
335
- devices$ = getAudioOutputDevices();
336
- break;
337
- default:
338
- throw new Error('Unknown MediaDeviceKind', kind);
339
- }
340
-
341
- return devices$.pipe(
342
- pairwise(),
343
- filter(([prev, current]) => {
344
- const prevDefault = prev.find((device) => device.deviceId === 'default');
345
- const currentDefault = current.find(
346
- (device) => device.deviceId === 'default',
347
- );
348
- return !!(
349
- current.length > prev.length &&
350
- prevDefault &&
351
- currentDefault &&
352
- prevDefault.groupId !== currentDefault.groupId
353
- );
354
- }),
355
- map(() => true),
356
- );
357
- };
358
-
359
- /**
360
- * Notifies the subscriber about newly added default audio input device.
361
- * @returns Observable<boolean>
362
- */
363
- export const watchForAddedDefaultAudioDevice = () =>
364
- watchForAddedDefaultDevice('audioinput');
365
-
366
- /**
367
- * Notifies the subscriber about newly added default audio output device.
368
- * @returns Observable<boolean>
369
- */
370
- export const watchForAddedDefaultAudioOutputDevice = () =>
371
- watchForAddedDefaultDevice('audiooutput');
372
-
373
- /**
374
- * Notifies the subscriber about newly added default video input device.
375
- * @returns Observable<boolean>
376
- */
377
- export const watchForAddedDefaultVideoDevice = () =>
378
- watchForAddedDefaultDevice('videoinput');
257
+ export const deviceIds$ =
258
+ typeof navigator !== 'undefined' &&
259
+ typeof navigator.mediaDevices !== 'undefined'
260
+ ? memoizedObservable(() =>
261
+ merge(
262
+ from(navigator.mediaDevices.enumerateDevices()),
263
+ getDeviceChangeObserver(),
264
+ ).pipe(shareReplay(1)),
265
+ )()
266
+ : undefined;
379
267
 
380
268
  /**
381
269
  * Deactivates MediaStream (stops and removes tracks) to be later garbage collected
@@ -44,25 +44,6 @@ const doJoin = async (
44
44
  ...data,
45
45
  location,
46
46
  };
47
-
48
- // FIXME OL: remove this once cascading is enabled by default
49
- const cascadingModeParams = getCascadingModeParams();
50
- if (cascadingModeParams) {
51
- // FIXME OL: remove after SFU migration is done
52
- if (data?.migrating_from && cascadingModeParams['next_sfu_id']) {
53
- cascadingModeParams['sfu_id'] = cascadingModeParams['next_sfu_id'];
54
- }
55
- return httpClient.doAxiosRequest<JoinCallResponse, JoinCallRequest>(
56
- 'post',
57
- `/call/${type}/${id}/join`,
58
- request,
59
- {
60
- params: {
61
- ...cascadingModeParams,
62
- },
63
- },
64
- );
65
- }
66
47
  return httpClient.post<JoinCallResponse, JoinCallRequest>(
67
48
  `/call/${type}/${id}/join`,
68
49
  request,
@@ -81,20 +62,6 @@ const toRtcConfiguration = (config?: ICEServer[]) => {
81
62
  return rtcConfig;
82
63
  };
83
64
 
84
- const getCascadingModeParams = () => {
85
- if (typeof window === 'undefined') return null;
86
- const params = new URLSearchParams(window.location?.search);
87
- const cascadingEnabled = params.get('cascading') !== null;
88
- if (cascadingEnabled) {
89
- const rawParams: Record<string, string> = {};
90
- params.forEach((value, key) => {
91
- rawParams[key] = value;
92
- });
93
- return rawParams;
94
- }
95
- return null;
96
- };
97
-
98
65
  /**
99
66
  * Reconciles the local state of the source participant into the target participant.
100
67
  *