@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.
- package/CHANGELOG.md +7 -0
- package/dist/index.browser.es.js +110 -93
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +109 -97
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +110 -93
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +6 -0
- package/dist/src/devices/SpeakerManager.d.ts +3 -0
- package/dist/src/devices/devices.d.ts +2 -41
- package/package.json +1 -1
- package/src/Call.ts +10 -1
- package/src/devices/InputMediaDeviceManager.ts +98 -4
- package/src/devices/SpeakerManager.ts +27 -1
- package/src/devices/__tests__/CameraManager.test.ts +7 -1
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +105 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +7 -1
- package/src/devices/__tests__/ScreenShareManager.test.ts +2 -1
- package/src/devices/__tests__/SpeakerManager.test.ts +12 -1
- package/src/devices/__tests__/mocks.ts +56 -7
- package/src/devices/devices.ts +11 -123
|
@@ -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
|
-
*
|
|
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
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(
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
};
|