@stream-io/video-client 0.4.5 → 0.4.7
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 +156 -99
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +155 -103
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +156 -99
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +8 -3
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +20 -7
- 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/CameraManagerState.ts +7 -2
- package/src/devices/InputMediaDeviceManager.ts +100 -7
- package/src/devices/InputMediaDeviceManagerState.ts +44 -1
- package/src/devices/MicrophoneManagerState.ts +7 -2
- package/src/devices/SpeakerManager.ts +27 -1
- package/src/devices/__tests__/CameraManager.test.ts +7 -1
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +113 -3
- package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +88 -0
- 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
|
|
@@ -49,13 +51,13 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
49
51
|
*/
|
|
50
52
|
setDefaultConstraints(constraints: C): void;
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
54
|
+
* Selects a device.
|
|
53
55
|
*
|
|
54
56
|
* Note: this method is not supported in React Native
|
|
55
|
-
*
|
|
56
|
-
* @param deviceId
|
|
57
|
+
* @param deviceId the device id to select.
|
|
57
58
|
*/
|
|
58
59
|
select(deviceId: string | undefined): Promise<void>;
|
|
60
|
+
removeSubscriptions: () => void;
|
|
59
61
|
protected applySettingsToStream(): Promise<void>;
|
|
60
62
|
protected abstract getDevices(): Observable<MediaDeviceInfo[] | undefined>;
|
|
61
63
|
protected abstract getStream(constraints: C): Promise<MediaStream>;
|
|
@@ -68,4 +70,7 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
68
70
|
private stopTracks;
|
|
69
71
|
private muteLocalStream;
|
|
70
72
|
protected unmuteStream(): Promise<void>;
|
|
73
|
+
private get mediaDeviceKind();
|
|
74
|
+
private handleDisconnectedOrReplacedDevices;
|
|
75
|
+
private findDeviceInList;
|
|
71
76
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BehaviorSubject } from 'rxjs';
|
|
1
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
2
2
|
import { RxUtils } from '../store';
|
|
3
3
|
export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
|
|
4
4
|
export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
|
|
5
5
|
readonly disableMode: 'stop-tracks' | 'disable-tracks';
|
|
6
|
+
private readonly permissionName;
|
|
6
7
|
protected statusSubject: BehaviorSubject<InputDeviceStatus>;
|
|
7
8
|
protected mediaStreamSubject: BehaviorSubject<MediaStream | undefined>;
|
|
8
9
|
protected selectedDeviceSubject: BehaviorSubject<string | undefined>;
|
|
@@ -15,20 +16,32 @@ export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstra
|
|
|
15
16
|
* An Observable that emits the current media stream, or `undefined` if the device is currently disabled.
|
|
16
17
|
*
|
|
17
18
|
*/
|
|
18
|
-
mediaStream$:
|
|
19
|
+
mediaStream$: Observable<MediaStream | undefined>;
|
|
19
20
|
/**
|
|
20
21
|
* An Observable that emits the currently selected device
|
|
21
22
|
*/
|
|
22
|
-
selectedDevice$:
|
|
23
|
+
selectedDevice$: Observable<string | undefined>;
|
|
23
24
|
/**
|
|
24
25
|
* An Observable that emits the device status
|
|
25
26
|
*/
|
|
26
|
-
status$:
|
|
27
|
+
status$: Observable<InputDeviceStatus>;
|
|
27
28
|
/**
|
|
28
29
|
* The default constraints for the device.
|
|
29
30
|
*/
|
|
30
|
-
defaultConstraints$:
|
|
31
|
-
|
|
31
|
+
defaultConstraints$: Observable<C | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* An observable that will emit `true` if browser/system permission
|
|
34
|
+
* is granted, `false` otherwise.
|
|
35
|
+
*/
|
|
36
|
+
hasBrowserPermission$: Observable<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Constructs new InputMediaDeviceManagerState instance.
|
|
39
|
+
*
|
|
40
|
+
* @param disableMode the disable mode to use.
|
|
41
|
+
* @param permissionName the permission name to use for querying.
|
|
42
|
+
* `undefined` means no permission is required.
|
|
43
|
+
*/
|
|
44
|
+
constructor(disableMode?: 'stop-tracks' | 'disable-tracks', permissionName?: PermissionName | undefined);
|
|
32
45
|
/**
|
|
33
46
|
* The device status
|
|
34
47
|
*/
|
|
@@ -47,7 +60,7 @@ export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstra
|
|
|
47
60
|
*
|
|
48
61
|
* @param observable$ the observable to get the value from.
|
|
49
62
|
*/
|
|
50
|
-
getCurrentValue: <T>(observable$:
|
|
63
|
+
getCurrentValue: <T>(observable$: Observable<T>) => T;
|
|
51
64
|
/**
|
|
52
65
|
* @internal
|
|
53
66
|
* @param status
|
|
@@ -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 { BehaviorSubject,
|
|
1
|
+
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
|
|
2
2
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
3
3
|
import { isReactNative } from '../helpers/platforms';
|
|
4
4
|
|
|
@@ -15,7 +15,12 @@ export class CameraManagerState extends InputMediaDeviceManagerState {
|
|
|
15
15
|
direction$: Observable<CameraDirection>;
|
|
16
16
|
|
|
17
17
|
constructor() {
|
|
18
|
-
super(
|
|
18
|
+
super(
|
|
19
|
+
'stop-tracks',
|
|
20
|
+
// `camera` is not in the W3C standard yet,
|
|
21
|
+
// but it's supported by Chrome and Safari.
|
|
22
|
+
'camera' as PermissionName,
|
|
23
|
+
);
|
|
19
24
|
this.direction$ = this.directionSubject
|
|
20
25
|
.asObservable()
|
|
21
26
|
.pipe(distinctUntilChanged());
|
|
@@ -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
|
/**
|
|
@@ -108,11 +118,10 @@ export abstract class InputMediaDeviceManager<
|
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/**
|
|
111
|
-
*
|
|
121
|
+
* Selects a device.
|
|
112
122
|
*
|
|
113
123
|
* Note: this method is not supported in React Native
|
|
114
|
-
*
|
|
115
|
-
* @param deviceId
|
|
124
|
+
* @param deviceId the device id to select.
|
|
116
125
|
*/
|
|
117
126
|
async select(deviceId: string | undefined) {
|
|
118
127
|
if (isReactNative()) {
|
|
@@ -125,6 +134,10 @@ export abstract class InputMediaDeviceManager<
|
|
|
125
134
|
await this.applySettingsToStream();
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
removeSubscriptions = () => {
|
|
138
|
+
this.subscriptions.forEach((s) => s.unsubscribe());
|
|
139
|
+
};
|
|
140
|
+
|
|
128
141
|
protected async applySettingsToStream() {
|
|
129
142
|
if (this.state.status === 'enabled') {
|
|
130
143
|
await this.muteStream();
|
|
@@ -204,9 +217,6 @@ export abstract class InputMediaDeviceManager<
|
|
|
204
217
|
stream = this.state.mediaStream;
|
|
205
218
|
this.unmuteTracks();
|
|
206
219
|
} else {
|
|
207
|
-
if (this.state.mediaStream) {
|
|
208
|
-
this.stopTracks();
|
|
209
|
-
}
|
|
210
220
|
const defaultConstraints = this.state.defaultConstraints;
|
|
211
221
|
const constraints: MediaTrackConstraints = {
|
|
212
222
|
...defaultConstraints,
|
|
@@ -219,6 +229,89 @@ export abstract class InputMediaDeviceManager<
|
|
|
219
229
|
}
|
|
220
230
|
if (this.state.mediaStream !== stream) {
|
|
221
231
|
this.state.setMediaStream(stream);
|
|
232
|
+
this.getTracks().forEach((track) => {
|
|
233
|
+
track.addEventListener('ended', async () => {
|
|
234
|
+
if (this.enablePromise) {
|
|
235
|
+
await this.enablePromise;
|
|
236
|
+
}
|
|
237
|
+
if (this.disablePromise) {
|
|
238
|
+
await this.disablePromise;
|
|
239
|
+
}
|
|
240
|
+
if (this.state.status === 'enabled') {
|
|
241
|
+
this.isTrackStoppedDueToTrackEnd = true;
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
this.isTrackStoppedDueToTrackEnd = false;
|
|
244
|
+
}, 2000);
|
|
245
|
+
await this.disable();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private get mediaDeviceKind() {
|
|
253
|
+
if (this.trackType === TrackType.AUDIO) {
|
|
254
|
+
return 'audioinput';
|
|
255
|
+
}
|
|
256
|
+
if (this.trackType === TrackType.VIDEO) {
|
|
257
|
+
return 'videoinput';
|
|
222
258
|
}
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private handleDisconnectedOrReplacedDevices() {
|
|
263
|
+
this.subscriptions.push(
|
|
264
|
+
combineLatest([
|
|
265
|
+
deviceIds$!.pipe(pairwise()),
|
|
266
|
+
this.state.selectedDevice$,
|
|
267
|
+
]).subscribe(async ([[prevDevices, currentDevices], deviceId]) => {
|
|
268
|
+
if (!deviceId) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (this.enablePromise) {
|
|
272
|
+
await this.enablePromise;
|
|
273
|
+
}
|
|
274
|
+
if (this.disablePromise) {
|
|
275
|
+
await this.disablePromise;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let isDeviceDisconnected = false;
|
|
279
|
+
let isDeviceReplaced = false;
|
|
280
|
+
const currentDevice = this.findDeviceInList(currentDevices, deviceId);
|
|
281
|
+
const prevDevice = this.findDeviceInList(prevDevices, deviceId);
|
|
282
|
+
if (!currentDevice && prevDevice) {
|
|
283
|
+
isDeviceDisconnected = true;
|
|
284
|
+
} else if (
|
|
285
|
+
currentDevice &&
|
|
286
|
+
prevDevice &&
|
|
287
|
+
currentDevice.deviceId === prevDevice.deviceId &&
|
|
288
|
+
currentDevice.groupId !== prevDevice.groupId
|
|
289
|
+
) {
|
|
290
|
+
isDeviceReplaced = true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (isDeviceDisconnected) {
|
|
294
|
+
await this.disable();
|
|
295
|
+
this.select(undefined);
|
|
296
|
+
}
|
|
297
|
+
if (isDeviceReplaced) {
|
|
298
|
+
if (
|
|
299
|
+
this.isTrackStoppedDueToTrackEnd &&
|
|
300
|
+
this.state.status === 'disabled'
|
|
301
|
+
) {
|
|
302
|
+
await this.enable();
|
|
303
|
+
this.isTrackStoppedDueToTrackEnd = false;
|
|
304
|
+
} else {
|
|
305
|
+
await this.applySettingsToStream();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private findDeviceInList(devices: MediaDeviceInfo[], deviceId: string) {
|
|
313
|
+
return devices.find(
|
|
314
|
+
(d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind,
|
|
315
|
+
);
|
|
223
316
|
}
|
|
224
317
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BehaviorSubject,
|
|
3
|
+
distinctUntilChanged,
|
|
4
|
+
Observable,
|
|
5
|
+
shareReplay,
|
|
6
|
+
} from 'rxjs';
|
|
7
|
+
import { isReactNative } from '../helpers/platforms';
|
|
2
8
|
import { RxUtils } from '../store';
|
|
3
9
|
|
|
4
10
|
export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
|
|
@@ -43,10 +49,47 @@ export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
|
|
|
43
49
|
*/
|
|
44
50
|
defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* An observable that will emit `true` if browser/system permission
|
|
54
|
+
* is granted, `false` otherwise.
|
|
55
|
+
*/
|
|
56
|
+
hasBrowserPermission$ = new Observable<boolean>((subscriber) => {
|
|
57
|
+
const notifyGranted = () => subscriber.next(true);
|
|
58
|
+
if (isReactNative() || !this.permissionName) return notifyGranted();
|
|
59
|
+
|
|
60
|
+
let permissionState: PermissionStatus;
|
|
61
|
+
const notify = () => subscriber.next(permissionState.state === 'granted');
|
|
62
|
+
navigator.permissions
|
|
63
|
+
.query({ name: this.permissionName })
|
|
64
|
+
.then((permissionStatus) => {
|
|
65
|
+
permissionState = permissionStatus;
|
|
66
|
+
permissionState.addEventListener('change', notify);
|
|
67
|
+
notify();
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {
|
|
70
|
+
// permission doesn't exist or can't be queried -> assume it's granted
|
|
71
|
+
// an example would be Firefox,
|
|
72
|
+
// where neither camera microphone permission can be queried
|
|
73
|
+
notifyGranted();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
permissionState?.removeEventListener('change', notify);
|
|
78
|
+
};
|
|
79
|
+
}).pipe(shareReplay(1));
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Constructs new InputMediaDeviceManagerState instance.
|
|
83
|
+
*
|
|
84
|
+
* @param disableMode the disable mode to use.
|
|
85
|
+
* @param permissionName the permission name to use for querying.
|
|
86
|
+
* `undefined` means no permission is required.
|
|
87
|
+
*/
|
|
46
88
|
constructor(
|
|
47
89
|
public readonly disableMode:
|
|
48
90
|
| 'stop-tracks'
|
|
49
91
|
| 'disable-tracks' = 'stop-tracks',
|
|
92
|
+
private readonly permissionName: PermissionName | undefined = undefined,
|
|
50
93
|
) {}
|
|
51
94
|
|
|
52
95
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BehaviorSubject,
|
|
1
|
+
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
|
|
2
2
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
3
3
|
|
|
4
4
|
export class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
@@ -12,7 +12,12 @@ export class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
|
12
12
|
speakingWhileMuted$: Observable<boolean>;
|
|
13
13
|
|
|
14
14
|
constructor() {
|
|
15
|
-
super(
|
|
15
|
+
super(
|
|
16
|
+
'disable-tracks',
|
|
17
|
+
// `microphone` is not in the W3C standard yet,
|
|
18
|
+
// but it's supported by Chrome and Safari.
|
|
19
|
+
'microphone' as PermissionName,
|
|
20
|
+
);
|
|
16
21
|
|
|
17
22
|
this.speakingWhileMuted$ = this.speakingWhileMutedSubject
|
|
18
23
|
.asObservable()
|
|
@@ -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> {
|
|
@@ -28,7 +44,14 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
|
|
|
28
44
|
public getTracks = () => this.state.mediaStream?.getTracks() ?? [];
|
|
29
45
|
|
|
30
46
|
constructor(call: Call) {
|
|
31
|
-
super(
|
|
47
|
+
super(
|
|
48
|
+
call,
|
|
49
|
+
new TestInputMediaDeviceManagerState(
|
|
50
|
+
'stop-tracks',
|
|
51
|
+
'camera' as PermissionName,
|
|
52
|
+
),
|
|
53
|
+
TrackType.VIDEO,
|
|
54
|
+
);
|
|
32
55
|
}
|
|
33
56
|
}
|
|
34
57
|
|
|
@@ -217,6 +240,93 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
217
240
|
});
|
|
218
241
|
});
|
|
219
242
|
|
|
243
|
+
it('should set status to disabled if track ends', async () => {
|
|
244
|
+
vi.useFakeTimers();
|
|
245
|
+
|
|
246
|
+
await manager.enable();
|
|
247
|
+
|
|
248
|
+
vi.spyOn(manager, 'enable');
|
|
249
|
+
vi.spyOn(manager, 'listDevices').mockImplementationOnce(() =>
|
|
250
|
+
of(mockVideoDevices.slice(1)),
|
|
251
|
+
);
|
|
252
|
+
await (
|
|
253
|
+
(manager.state.mediaStream?.getTracks()[0] as MockTrack).eventHandlers[
|
|
254
|
+
'ended'
|
|
255
|
+
] as Function
|
|
256
|
+
)();
|
|
257
|
+
await vi.runAllTimersAsync();
|
|
258
|
+
|
|
259
|
+
expect(manager.state.status).toBe('disabled');
|
|
260
|
+
expect(manager.enable).not.toHaveBeenCalled();
|
|
261
|
+
|
|
262
|
+
vi.useRealTimers();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should restart track if the default device is replaced and status is enabled', async () => {
|
|
266
|
+
vi.useFakeTimers();
|
|
267
|
+
emitDeviceIds(mockVideoDevices);
|
|
268
|
+
|
|
269
|
+
await manager.enable();
|
|
270
|
+
const device = mockVideoDevices[0];
|
|
271
|
+
await manager.select(device.deviceId);
|
|
272
|
+
|
|
273
|
+
//@ts-expect-error
|
|
274
|
+
vi.spyOn(manager, 'applySettingsToStream');
|
|
275
|
+
|
|
276
|
+
emitDeviceIds([
|
|
277
|
+
{ ...device, groupId: device.groupId + 'new' },
|
|
278
|
+
...mockVideoDevices.slice(1),
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
await vi.runAllTimersAsync();
|
|
282
|
+
|
|
283
|
+
expect(manager['applySettingsToStream']).toHaveBeenCalledOnce();
|
|
284
|
+
expect(manager.state.status).toBe('enabled');
|
|
285
|
+
|
|
286
|
+
vi.useRealTimers();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should do nothing if default device is replaced and status is disabled', async () => {
|
|
290
|
+
vi.useFakeTimers();
|
|
291
|
+
emitDeviceIds(mockVideoDevices);
|
|
292
|
+
|
|
293
|
+
const device = mockVideoDevices[0];
|
|
294
|
+
await manager.select(device.deviceId);
|
|
295
|
+
await manager.disable();
|
|
296
|
+
|
|
297
|
+
emitDeviceIds([
|
|
298
|
+
{ ...device, groupId: device.groupId + 'new' },
|
|
299
|
+
...mockVideoDevices.slice(1),
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
await vi.runAllTimersAsync();
|
|
303
|
+
|
|
304
|
+
expect(manager.state.status).toBe('disabled');
|
|
305
|
+
expect(manager.disablePromise).toBeUndefined();
|
|
306
|
+
expect(manager.enablePromise).toBeUndefined();
|
|
307
|
+
expect(manager.state.selectedDevice).toBe(device.deviceId);
|
|
308
|
+
|
|
309
|
+
vi.useRealTimers();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should disable stream and deselect device if selected device is disconnected', async () => {
|
|
313
|
+
vi.useFakeTimers();
|
|
314
|
+
emitDeviceIds(mockVideoDevices);
|
|
315
|
+
|
|
316
|
+
await manager.enable();
|
|
317
|
+
const device = mockVideoDevices[0];
|
|
318
|
+
await manager.select(device.deviceId);
|
|
319
|
+
|
|
320
|
+
emitDeviceIds(mockVideoDevices.slice(1));
|
|
321
|
+
|
|
322
|
+
await vi.runAllTimersAsync();
|
|
323
|
+
|
|
324
|
+
expect(manager.state.selectedDevice).toBe(undefined);
|
|
325
|
+
expect(manager.state.status).toBe('disabled');
|
|
326
|
+
|
|
327
|
+
vi.useRealTimers();
|
|
328
|
+
});
|
|
329
|
+
|
|
220
330
|
afterEach(() => {
|
|
221
331
|
vi.clearAllMocks();
|
|
222
332
|
vi.resetModules();
|