@stream-io/video-client 0.4.5 → 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.
@@ -16,6 +16,8 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
16
16
  */
17
17
  disablePromise?: Promise<void>;
18
18
  logger: Logger;
19
+ private subscriptions;
20
+ private isTrackStoppedDueToTrackEnd;
19
21
  protected constructor(call: Call, state: T, trackType: TrackType);
20
22
  /**
21
23
  * Lists the available audio/video devices
@@ -56,6 +58,7 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
56
58
  * @param deviceId
57
59
  */
58
60
  select(deviceId: string | undefined): Promise<void>;
61
+ removeSubscriptions: () => void;
59
62
  protected applySettingsToStream(): Promise<void>;
60
63
  protected abstract getDevices(): Observable<MediaDeviceInfo[] | undefined>;
61
64
  protected abstract getStream(constraints: C): Promise<MediaStream>;
@@ -68,4 +71,7 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
68
71
  private stopTracks;
69
72
  private muteLocalStream;
70
73
  protected unmuteStream(): Promise<void>;
74
+ private get mediaDeviceKind();
75
+ private handleDisconnectedOrReplacedDevices;
76
+ private findDeviceInList;
71
77
  }
@@ -1,6 +1,8 @@
1
1
  import { SpeakerState } from './SpeakerState';
2
2
  export declare class SpeakerManager {
3
3
  readonly state: SpeakerState;
4
+ private subscriptions;
5
+ constructor();
4
6
  /**
5
7
  * Lists the available audio output devices
6
8
  *
@@ -17,6 +19,7 @@ export declare class SpeakerManager {
17
19
  * @param deviceId empty string means the system default
18
20
  */
19
21
  select(deviceId: string): void;
22
+ removeSubscriptions: () => void;
20
23
  /**
21
24
  * Set the volume of the audio elements
22
25
  * @param volume a number between 0 and 1
@@ -2,8 +2,7 @@ import { Observable } from 'rxjs';
2
2
  /**
3
3
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
4
4
  *
5
- * @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.
6
- */
5
+ * */
7
6
  export declare const checkIfAudioOutputChangeSupported: () => boolean;
8
7
  /**
9
8
  * Prompts the user for a permission to use audio devices (if not already granted) and lists the available 'audioinput' devices, if devices are added/removed the list is updated.
@@ -46,45 +45,7 @@ export declare const getVideoStream: (trackConstraints?: MediaTrackConstraints)
46
45
  * @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
47
46
  */
48
47
  export declare const getScreenShareStream: (options?: DisplayMediaStreamOptions) => Promise<MediaStream>;
49
- /**
50
- * Notifies the subscriber if a given 'audioinput' device is disconnected
51
- *
52
- * @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.
53
- * @param deviceId$ an Observable that specifies which device to watch for
54
- * @returns
55
- */
56
- export declare const watchForDisconnectedAudioDevice: (deviceId$: Observable<string | undefined>) => Observable<boolean>;
57
- /**
58
- * Notifies the subscriber if a given 'videoinput' device is disconnected
59
- *
60
- * @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.
61
- * @param deviceId$ an Observable that specifies which device to watch for
62
- * @returns
63
- */
64
- export declare const watchForDisconnectedVideoDevice: (deviceId$: Observable<string | undefined>) => Observable<boolean>;
65
- /**
66
- * Notifies the subscriber if a given 'audiooutput' device is disconnected
67
- *
68
- * @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.
69
- * @param deviceId$ an Observable that specifies which device to watch for
70
- * @returns
71
- */
72
- export declare const watchForDisconnectedAudioOutputDevice: (deviceId$: Observable<string | undefined>) => Observable<boolean>;
73
- /**
74
- * Notifies the subscriber about newly added default audio input device.
75
- * @returns Observable<boolean>
76
- */
77
- export declare const watchForAddedDefaultAudioDevice: () => Observable<boolean>;
78
- /**
79
- * Notifies the subscriber about newly added default audio output device.
80
- * @returns Observable<boolean>
81
- */
82
- export declare const watchForAddedDefaultAudioOutputDevice: () => Observable<boolean>;
83
- /**
84
- * Notifies the subscriber about newly added default video input device.
85
- * @returns Observable<boolean>
86
- */
87
- export declare const watchForAddedDefaultVideoDevice: () => Observable<boolean>;
48
+ export declare const deviceIds$: Observable<MediaDeviceInfo[]> | undefined;
88
49
  /**
89
50
  * Deactivates MediaStream (stops and removes tracks) to be later garbage collected
90
51
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
package/src/Call.ts CHANGED
@@ -507,6 +507,11 @@ export class Call {
507
507
 
508
508
  this.clientStore.unregisterCall(this);
509
509
  this.state.setCallingState(CallingState.LEFT);
510
+
511
+ this.camera.removeSubscriptions();
512
+ this.microphone.removeSubscriptions();
513
+ this.screenShare.removeSubscriptions();
514
+ this.speaker.removeSubscriptions();
510
515
  };
511
516
 
512
517
  /**
@@ -1004,7 +1009,11 @@ export class Call {
1004
1009
  await this.initCamera({ setStatus: true });
1005
1010
  await this.initMic({ setStatus: true });
1006
1011
  } catch (error) {
1007
- this.logger('warn', 'Camera and/or mic init failed during join call');
1012
+ this.logger(
1013
+ 'warn',
1014
+ 'Camera and/or mic init failed during join call',
1015
+ error,
1016
+ );
1008
1017
  }
1009
1018
 
1010
1019
  // 3. once we have the "joinResponse", and possibly reconciled the local state
@@ -1,4 +1,4 @@
1
- import { Observable } from 'rxjs';
1
+ import { Observable, Subscription, combineLatest, pairwise } from 'rxjs';
2
2
  import { Call } from '../Call';
3
3
  import { CallingState } from '../store';
4
4
  import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
@@ -6,6 +6,7 @@ import { isReactNative } from '../helpers/platforms';
6
6
  import { Logger } from '../coordinator/connection/types';
7
7
  import { getLogger } from '../logger';
8
8
  import { TrackType } from '../gen/video/sfu/models/models';
9
+ import { deviceIds$ } from './devices';
9
10
 
10
11
  export abstract class InputMediaDeviceManager<
11
12
  T extends InputMediaDeviceManagerState<C>,
@@ -20,6 +21,8 @@ export abstract class InputMediaDeviceManager<
20
21
  */
21
22
  disablePromise?: Promise<void>;
22
23
  logger: Logger;
24
+ private subscriptions: Subscription[] = [];
25
+ private isTrackStoppedDueToTrackEnd = false;
23
26
 
24
27
  protected constructor(
25
28
  protected readonly call: Call,
@@ -27,6 +30,13 @@ export abstract class InputMediaDeviceManager<
27
30
  protected readonly trackType: TrackType,
28
31
  ) {
29
32
  this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
33
+ if (
34
+ deviceIds$ &&
35
+ !isReactNative() &&
36
+ (this.trackType === TrackType.AUDIO || this.trackType === TrackType.VIDEO)
37
+ ) {
38
+ this.handleDisconnectedOrReplacedDevices();
39
+ }
30
40
  }
31
41
 
32
42
  /**
@@ -125,6 +135,10 @@ export abstract class InputMediaDeviceManager<
125
135
  await this.applySettingsToStream();
126
136
  }
127
137
 
138
+ removeSubscriptions = () => {
139
+ this.subscriptions.forEach((s) => s.unsubscribe());
140
+ };
141
+
128
142
  protected async applySettingsToStream() {
129
143
  if (this.state.status === 'enabled') {
130
144
  await this.muteStream();
@@ -204,9 +218,6 @@ export abstract class InputMediaDeviceManager<
204
218
  stream = this.state.mediaStream;
205
219
  this.unmuteTracks();
206
220
  } else {
207
- if (this.state.mediaStream) {
208
- this.stopTracks();
209
- }
210
221
  const defaultConstraints = this.state.defaultConstraints;
211
222
  const constraints: MediaTrackConstraints = {
212
223
  ...defaultConstraints,
@@ -219,6 +230,89 @@ export abstract class InputMediaDeviceManager<
219
230
  }
220
231
  if (this.state.mediaStream !== stream) {
221
232
  this.state.setMediaStream(stream);
233
+ this.getTracks().forEach((track) => {
234
+ track.addEventListener('ended', async () => {
235
+ if (this.enablePromise) {
236
+ await this.enablePromise;
237
+ }
238
+ if (this.disablePromise) {
239
+ await this.disablePromise;
240
+ }
241
+ if (this.state.status === 'enabled') {
242
+ this.isTrackStoppedDueToTrackEnd = true;
243
+ setTimeout(() => {
244
+ this.isTrackStoppedDueToTrackEnd = false;
245
+ }, 2000);
246
+ await this.disable();
247
+ }
248
+ });
249
+ });
250
+ }
251
+ }
252
+
253
+ private get mediaDeviceKind() {
254
+ if (this.trackType === TrackType.AUDIO) {
255
+ return 'audioinput';
222
256
  }
257
+ if (this.trackType === TrackType.VIDEO) {
258
+ return 'videoinput';
259
+ }
260
+ return '';
261
+ }
262
+
263
+ private handleDisconnectedOrReplacedDevices() {
264
+ this.subscriptions.push(
265
+ combineLatest([
266
+ deviceIds$!.pipe(pairwise()),
267
+ this.state.selectedDevice$,
268
+ ]).subscribe(async ([[prevDevices, currentDevices], deviceId]) => {
269
+ if (!deviceId) {
270
+ return;
271
+ }
272
+ if (this.enablePromise) {
273
+ await this.enablePromise;
274
+ }
275
+ if (this.disablePromise) {
276
+ await this.disablePromise;
277
+ }
278
+
279
+ let isDeviceDisconnected = false;
280
+ let isDeviceReplaced = false;
281
+ const currentDevice = this.findDeviceInList(currentDevices, deviceId);
282
+ const prevDevice = this.findDeviceInList(prevDevices, deviceId);
283
+ if (!currentDevice && prevDevice) {
284
+ isDeviceDisconnected = true;
285
+ } else if (
286
+ currentDevice &&
287
+ prevDevice &&
288
+ currentDevice.deviceId === prevDevice.deviceId &&
289
+ currentDevice.groupId !== prevDevice.groupId
290
+ ) {
291
+ isDeviceReplaced = true;
292
+ }
293
+
294
+ if (isDeviceDisconnected) {
295
+ await this.disable();
296
+ this.select(undefined);
297
+ }
298
+ if (isDeviceReplaced) {
299
+ if (
300
+ this.isTrackStoppedDueToTrackEnd &&
301
+ this.state.status === 'disabled'
302
+ ) {
303
+ await this.enable();
304
+ this.isTrackStoppedDueToTrackEnd = false;
305
+ } else {
306
+ await this.applySettingsToStream();
307
+ }
308
+ }
309
+ }),
310
+ );
311
+ }
312
+
313
+ private findDeviceInList(devices: MediaDeviceInfo[], deviceId: string) {
314
+ return devices.find(
315
+ (d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind,
316
+ );
223
317
  }
224
318
  }
@@ -1,9 +1,31 @@
1
+ import { Subscription, combineLatest } from 'rxjs';
1
2
  import { isReactNative } from '../helpers/platforms';
2
3
  import { SpeakerState } from './SpeakerState';
3
- import { getAudioOutputDevices } from './devices';
4
+ import { deviceIds$, getAudioOutputDevices } from './devices';
4
5
 
5
6
  export class SpeakerManager {
6
7
  public readonly state = new SpeakerState();
8
+ private subscriptions: Subscription[] = [];
9
+
10
+ constructor() {
11
+ if (deviceIds$ && !isReactNative()) {
12
+ this.subscriptions.push(
13
+ combineLatest([deviceIds$!, this.state.selectedDevice$]).subscribe(
14
+ ([devices, deviceId]) => {
15
+ if (!deviceId) {
16
+ return;
17
+ }
18
+ const device = devices.find(
19
+ (d) => d.deviceId === deviceId && d.kind === 'audiooutput',
20
+ );
21
+ if (!device) {
22
+ this.select('');
23
+ }
24
+ },
25
+ ),
26
+ );
27
+ }
28
+ }
7
29
 
8
30
  /**
9
31
  * Lists the available audio output devices
@@ -30,6 +52,10 @@ export class SpeakerManager {
30
52
  this.state.setDevice(deviceId);
31
53
  }
32
54
 
55
+ removeSubscriptions = () => {
56
+ this.subscriptions.forEach((s) => s.unsubscribe());
57
+ };
58
+
33
59
  /**
34
60
  * Set the volume of the audio elements
35
61
  * @param volume a number between 0 and 1
@@ -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 { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
6
+ import {
7
+ mockCall,
8
+ mockDeviceIds$,
9
+ mockVideoDevices,
10
+ mockVideoStream,
11
+ } from './mocks';
7
12
  import { getVideoStream } from '../devices';
8
13
  import { TrackType } from '../../gen/video/sfu/models/models';
9
14
  import { CameraManager } from '../CameraManager';
@@ -17,6 +22,7 @@ vi.mock('../devices.ts', () => {
17
22
  return of(mockVideoDevices);
18
23
  }),
19
24
  getVideoStream: vi.fn(() => Promise.resolve(mockVideoStream())),
25
+ deviceIds$: mockDeviceIds$(),
20
26
  };
21
27
  });
22
28
 
@@ -3,7 +3,14 @@ import { StreamClient } from '../../coordinator/connection/client';
3
3
  import { CallingState, StreamVideoWriteableStateStore } from '../../store';
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
6
+ import {
7
+ MockTrack,
8
+ emitDeviceIds,
9
+ mockCall,
10
+ mockDeviceIds$,
11
+ mockVideoDevices,
12
+ mockVideoStream,
13
+ } from './mocks';
7
14
  import { InputMediaDeviceManager } from '../InputMediaDeviceManager';
8
15
  import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
9
16
  import { of } from 'rxjs';
@@ -16,8 +23,17 @@ vi.mock('../../Call.ts', () => {
16
23
  };
17
24
  });
18
25
 
26
+ vi.mock('../devices.ts', () => {
27
+ console.log('MOCKING devices API');
28
+ return {
29
+ deviceIds$: mockDeviceIds$(),
30
+ };
31
+ });
32
+
19
33
  class TestInputMediaDeviceManagerState extends InputMediaDeviceManagerState {
20
- public getDeviceIdFromStream = vi.fn(() => 'mock-device-id');
34
+ public getDeviceIdFromStream = vi.fn(
35
+ (stream) => stream.getVideoTracks()[0].getSettings().deviceId,
36
+ );
21
37
  }
22
38
 
23
39
  class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMediaDeviceManagerState> {
@@ -217,6 +233,93 @@ describe('InputMediaDeviceManager.test', () => {
217
233
  });
218
234
  });
219
235
 
236
+ it('should set status to disabled if track ends', async () => {
237
+ vi.useFakeTimers();
238
+
239
+ await manager.enable();
240
+
241
+ vi.spyOn(manager, 'enable');
242
+ vi.spyOn(manager, 'listDevices').mockImplementationOnce(() =>
243
+ of(mockVideoDevices.slice(1)),
244
+ );
245
+ await (
246
+ (manager.state.mediaStream?.getTracks()[0] as MockTrack).eventHandlers[
247
+ 'ended'
248
+ ] as Function
249
+ )();
250
+ await vi.runAllTimersAsync();
251
+
252
+ expect(manager.state.status).toBe('disabled');
253
+ expect(manager.enable).not.toHaveBeenCalled();
254
+
255
+ vi.useRealTimers();
256
+ });
257
+
258
+ it('should restart track if the default device is replaced and status is enabled', async () => {
259
+ vi.useFakeTimers();
260
+ emitDeviceIds(mockVideoDevices);
261
+
262
+ await manager.enable();
263
+ const device = mockVideoDevices[0];
264
+ await manager.select(device.deviceId);
265
+
266
+ //@ts-expect-error
267
+ vi.spyOn(manager, 'applySettingsToStream');
268
+
269
+ emitDeviceIds([
270
+ { ...device, groupId: device.groupId + 'new' },
271
+ ...mockVideoDevices.slice(1),
272
+ ]);
273
+
274
+ await vi.runAllTimersAsync();
275
+
276
+ expect(manager['applySettingsToStream']).toHaveBeenCalledOnce();
277
+ expect(manager.state.status).toBe('enabled');
278
+
279
+ vi.useRealTimers();
280
+ });
281
+
282
+ it('should do nothing if default device is replaced and status is disabled', async () => {
283
+ vi.useFakeTimers();
284
+ emitDeviceIds(mockVideoDevices);
285
+
286
+ const device = mockVideoDevices[0];
287
+ await manager.select(device.deviceId);
288
+ await manager.disable();
289
+
290
+ emitDeviceIds([
291
+ { ...device, groupId: device.groupId + 'new' },
292
+ ...mockVideoDevices.slice(1),
293
+ ]);
294
+
295
+ await vi.runAllTimersAsync();
296
+
297
+ expect(manager.state.status).toBe('disabled');
298
+ expect(manager.disablePromise).toBeUndefined();
299
+ expect(manager.enablePromise).toBeUndefined();
300
+ expect(manager.state.selectedDevice).toBe(device.deviceId);
301
+
302
+ vi.useRealTimers();
303
+ });
304
+
305
+ it('should disable stream and deselect device if selected device is disconnected', async () => {
306
+ vi.useFakeTimers();
307
+ emitDeviceIds(mockVideoDevices);
308
+
309
+ await manager.enable();
310
+ const device = mockVideoDevices[0];
311
+ await manager.select(device.deviceId);
312
+
313
+ emitDeviceIds(mockVideoDevices.slice(1));
314
+
315
+ await vi.runAllTimersAsync();
316
+
317
+ expect(manager.state.selectedDevice).toBe(undefined);
318
+ expect(manager.state.status).toBe('disabled');
319
+
320
+ vi.useRealTimers();
321
+ });
322
+
220
323
  afterEach(() => {
221
324
  vi.clearAllMocks();
222
325
  vi.resetModules();
@@ -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
  };