@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.
@@ -51,11 +51,10 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
51
51
  */
52
52
  setDefaultConstraints(constraints: C): void;
53
53
  /**
54
- * Select device
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$: import("rxjs").Observable<MediaStream | undefined>;
19
+ mediaStream$: Observable<MediaStream | undefined>;
19
20
  /**
20
21
  * An Observable that emits the currently selected device
21
22
  */
22
- selectedDevice$: import("rxjs").Observable<string | undefined>;
23
+ selectedDevice$: Observable<string | undefined>;
23
24
  /**
24
25
  * An Observable that emits the device status
25
26
  */
26
- status$: import("rxjs").Observable<InputDeviceStatus>;
27
+ status$: Observable<InputDeviceStatus>;
27
28
  /**
28
29
  * The default constraints for the device.
29
30
  */
30
- defaultConstraints$: import("rxjs").Observable<C | undefined>;
31
- constructor(disableMode?: 'stop-tracks' | 'disable-tracks');
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$: import("rxjs").Observable<T>) => T;
63
+ getCurrentValue: <T>(observable$: Observable<T>) => T;
51
64
  /**
52
65
  * @internal
53
66
  * @param status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
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('stop-tracks');
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
- * Select device
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 { BehaviorSubject, distinctUntilChanged } from 'rxjs';
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, Observable, distinctUntilChanged } from 'rxjs';
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('disable-tracks');
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(call, new TestInputMediaDeviceManagerState(), TrackType.VIDEO);
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
+ });