@stream-io/video-client 0.4.6 → 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 +7 -0
- package/dist/index.browser.es.js +47 -7
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +47 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +47 -7
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +2 -3
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +20 -7
- package/package.json +1 -1
- package/src/devices/CameraManagerState.ts +7 -2
- package/src/devices/InputMediaDeviceManager.ts +2 -3
- package/src/devices/InputMediaDeviceManagerState.ts +44 -1
- package/src/devices/MicrophoneManagerState.ts +7 -2
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +8 -1
- package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +88 -0
|
@@ -51,11 +51,10 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
51
51
|
*/
|
|
52
52
|
setDefaultConstraints(constraints: C): void;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* Selects a device.
|
|
55
55
|
*
|
|
56
56
|
* Note: this method is not supported in React Native
|
|
57
|
-
*
|
|
58
|
-
* @param deviceId
|
|
57
|
+
* @param deviceId the device id to select.
|
|
59
58
|
*/
|
|
60
59
|
select(deviceId: string | undefined): Promise<void>;
|
|
61
60
|
removeSubscriptions: () => void;
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -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());
|
|
@@ -118,11 +118,10 @@ export abstract class InputMediaDeviceManager<
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
121
|
+
* Selects a device.
|
|
122
122
|
*
|
|
123
123
|
* Note: this method is not supported in React Native
|
|
124
|
-
*
|
|
125
|
-
* @param deviceId
|
|
124
|
+
* @param deviceId the device id to select.
|
|
126
125
|
*/
|
|
127
126
|
async select(deviceId: string | undefined) {
|
|
128
127
|
if (isReactNative()) {
|
|
@@ -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()
|
|
@@ -44,7 +44,14 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
|
|
|
44
44
|
public getTracks = () => this.state.mediaStream?.getTracks() ?? [];
|
|
45
45
|
|
|
46
46
|
constructor(call: Call) {
|
|
47
|
-
super(
|
|
47
|
+
super(
|
|
48
|
+
call,
|
|
49
|
+
new TestInputMediaDeviceManagerState(
|
|
50
|
+
'stop-tracks',
|
|
51
|
+
'camera' as PermissionName,
|
|
52
|
+
),
|
|
53
|
+
TrackType.VIDEO,
|
|
54
|
+
);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
|
|
3
|
+
|
|
4
|
+
class TestInputMediaDeviceManagerState extends InputMediaDeviceManagerState {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('stop-tracks', 'camera' as PermissionName);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getDeviceIdFromStream = vi.fn();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('InputMediaDeviceManagerState', () => {
|
|
13
|
+
let state: InputMediaDeviceManagerState;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
state = new TestInputMediaDeviceManagerState();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('hasBrowserPermission', () => {
|
|
20
|
+
it('should emit true when permission is granted', async () => {
|
|
21
|
+
const permissionStatus: Partial<PermissionStatus> = {
|
|
22
|
+
state: 'granted',
|
|
23
|
+
addEventListener: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
const query = vi.fn(() => Promise.resolve(permissionStatus));
|
|
26
|
+
globalThis.navigator ??= {} as Navigator;
|
|
27
|
+
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
28
|
+
globalThis.navigator.permissions = { query };
|
|
29
|
+
|
|
30
|
+
const hasPermission = await new Promise((resolve) => {
|
|
31
|
+
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
32
|
+
});
|
|
33
|
+
expect(hasPermission).toBe(true);
|
|
34
|
+
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
35
|
+
expect(permissionStatus.addEventListener).toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it(' should emit false when permission is denied', async () => {
|
|
39
|
+
const permissionStatus: Partial<PermissionStatus> = {
|
|
40
|
+
state: 'denied',
|
|
41
|
+
addEventListener: vi.fn(),
|
|
42
|
+
removeEventListener: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
const query = vi.fn(() => Promise.resolve(permissionStatus));
|
|
45
|
+
globalThis.navigator ??= {} as Navigator;
|
|
46
|
+
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
47
|
+
globalThis.navigator.permissions = { query };
|
|
48
|
+
|
|
49
|
+
const hasPermission = await new Promise((resolve) => {
|
|
50
|
+
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
51
|
+
});
|
|
52
|
+
expect(hasPermission).toBe(false);
|
|
53
|
+
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
54
|
+
expect(permissionStatus.addEventListener).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should emit false when prompt is needed', async () => {
|
|
58
|
+
const permissionStatus: Partial<PermissionStatus> = {
|
|
59
|
+
state: 'prompt',
|
|
60
|
+
addEventListener: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
const query = vi.fn(() => Promise.resolve(permissionStatus));
|
|
63
|
+
globalThis.navigator ??= {} as Navigator;
|
|
64
|
+
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
65
|
+
globalThis.navigator.permissions = { query };
|
|
66
|
+
|
|
67
|
+
const hasPermission = await new Promise((resolve) => {
|
|
68
|
+
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
69
|
+
});
|
|
70
|
+
expect(hasPermission).toBe(false);
|
|
71
|
+
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
72
|
+
expect(permissionStatus.addEventListener).toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should emit true when permissions cannot be queried', async () => {
|
|
76
|
+
const query = vi.fn(() => Promise.reject());
|
|
77
|
+
globalThis.navigator ??= {} as Navigator;
|
|
78
|
+
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
79
|
+
globalThis.navigator.permissions = { query };
|
|
80
|
+
|
|
81
|
+
const hasPermission = await new Promise((resolve) => {
|
|
82
|
+
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
83
|
+
});
|
|
84
|
+
expect(hasPermission).toBe(true);
|
|
85
|
+
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|