@stream-io/video-client 1.4.1 → 1.4.3

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +219 -144
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +219 -142
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +219 -144
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +7 -7
  9. package/dist/src/StreamSfuClient.d.ts +7 -7
  10. package/dist/src/StreamVideoClient.d.ts +5 -5
  11. package/dist/src/coordinator/connection/client.d.ts +13 -14
  12. package/dist/src/coordinator/connection/connection.d.ts +3 -5
  13. package/dist/src/coordinator/connection/insights.d.ts +0 -1
  14. package/dist/src/devices/BrowserPermission.d.ts +24 -0
  15. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +3 -3
  16. package/dist/src/devices/devices.d.ts +30 -11
  17. package/dist/src/gen/coordinator/index.d.ts +5 -0
  18. package/dist/src/helpers/ViewportTracker.d.ts +1 -1
  19. package/dist/src/helpers/lazy.d.ts +4 -0
  20. package/dist/src/helpers/sdp-munging.d.ts +2 -2
  21. package/dist/src/rtc/Dispatcher.d.ts +2 -2
  22. package/dist/src/rtc/codecs.d.ts +1 -1
  23. package/dist/src/rtc/signal.d.ts +0 -1
  24. package/dist/src/stats/utils.d.ts +4 -4
  25. package/dist/src/store/CallState.d.ts +1 -1
  26. package/package.json +4 -4
  27. package/src/devices/BrowserPermission.ts +152 -0
  28. package/src/devices/CameraManagerState.ts +2 -6
  29. package/src/devices/InputMediaDeviceManagerState.ts +10 -44
  30. package/src/devices/MicrophoneManagerState.ts +2 -6
  31. package/src/devices/__tests__/CameraManager.test.ts +3 -0
  32. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +5 -3
  33. package/src/devices/__tests__/InputMediaDeviceManagerFilters.test.ts +6 -2
  34. package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +41 -51
  35. package/src/devices/__tests__/MicrophoneManager.test.ts +4 -1
  36. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +8 -1
  37. package/src/devices/__tests__/SpeakerManager.test.ts +8 -1
  38. package/src/devices/__tests__/mocks.ts +6 -1
  39. package/src/devices/devices.ts +113 -112
  40. package/src/gen/coordinator/index.ts +5 -0
  41. package/src/helpers/RNSpeechDetector.ts +1 -1
  42. package/src/helpers/lazy.ts +15 -0
@@ -2,11 +2,11 @@ import {
2
2
  BehaviorSubject,
3
3
  distinctUntilChanged,
4
4
  Observable,
5
+ of,
5
6
  shareReplay,
6
7
  } from 'rxjs';
7
- import { isReactNative } from '../helpers/platforms';
8
8
  import { RxUtils } from '../store';
9
- import { getLogger } from '../logger';
9
+ import { BrowserPermission } from './BrowserPermission';
10
10
 
11
11
  export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
12
12
  export type TrackDisableMode = 'stop-tracks' | 'disable-tracks';
@@ -65,57 +65,23 @@ export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
65
65
  * An observable that will emit `true` if browser/system permission
66
66
  * is granted, `false` otherwise.
67
67
  */
68
- hasBrowserPermission$ = new Observable<boolean>((subscriber) => {
69
- const notifyGranted = () => subscriber.next(true);
70
- const permissionsAPIAvailable = !!navigator?.permissions?.query;
71
- if (isReactNative() || !this.permissionName || !permissionsAPIAvailable) {
72
- getLogger(['devices'])(
73
- 'warn',
74
- `Permissions can't be queried. Assuming granted.`,
75
- );
76
- return notifyGranted();
77
- }
78
-
79
- let permissionState: PermissionStatus;
80
- const notify = () => {
81
- subscriber.next(
82
- // In some browsers, the 'change' event doesn't reliably emit and hence,
83
- // permissionState stays in 'prompt' state forever.
84
- // Typically, this happens when a user grants one-time permission.
85
- // Instead of checking if a permission is granted, we check if it isn't denied
86
- permissionState.state !== 'denied',
87
- );
88
- };
89
- navigator.permissions
90
- .query({ name: this.permissionName })
91
- .then((permissionStatus) => {
92
- permissionState = permissionStatus;
93
- permissionState.addEventListener('change', notify);
94
- notify();
95
- })
96
- .catch(() => {
97
- // permission doesn't exist or can't be queried -> assume it's granted
98
- // an example would be Firefox,
99
- // where neither camera microphone permission can be queried
100
- notifyGranted();
101
- });
102
-
103
- return () => {
104
- permissionState?.removeEventListener('change', notify);
105
- };
106
- }).pipe(shareReplay(1));
68
+ hasBrowserPermission$: Observable<boolean>;
107
69
 
108
70
  /**
109
71
  * Constructs new InputMediaDeviceManagerState instance.
110
72
  *
111
73
  * @param disableMode the disable mode to use.
112
- * @param permissionName the permission name to use for querying.
74
+ * @param permission the BrowserPermission to use for querying.
113
75
  * `undefined` means no permission is required.
114
76
  */
115
77
  constructor(
116
78
  public readonly disableMode: TrackDisableMode = 'stop-tracks',
117
- private readonly permissionName: PermissionName | undefined = undefined,
118
- ) {}
79
+ permission?: BrowserPermission,
80
+ ) {
81
+ this.hasBrowserPermission$ = permission
82
+ ? permission.asObservable().pipe(shareReplay(1))
83
+ : of(true);
84
+ }
119
85
 
120
86
  /**
121
87
  * The device status
@@ -3,6 +3,7 @@ import {
3
3
  InputMediaDeviceManagerState,
4
4
  TrackDisableMode,
5
5
  } from './InputMediaDeviceManagerState';
6
+ import { getAudioBrowserPermission } from './devices';
6
7
 
7
8
  export class MicrophoneManagerState extends InputMediaDeviceManagerState {
8
9
  private speakingWhileMutedSubject = new BehaviorSubject<boolean>(false);
@@ -15,12 +16,7 @@ export class MicrophoneManagerState extends InputMediaDeviceManagerState {
15
16
  speakingWhileMuted$: Observable<boolean>;
16
17
 
17
18
  constructor(disableMode: TrackDisableMode) {
18
- super(
19
- disableMode,
20
- // `microphone` is not in the W3C standard yet,
21
- // but it's supported by Chrome and Safari.
22
- 'microphone' as PermissionName,
23
- );
19
+ super(disableMode, getAudioBrowserPermission());
24
20
 
25
21
  this.speakingWhileMuted$ = this.speakingWhileMutedSubject
26
22
  .asObservable()
@@ -4,6 +4,7 @@ import { CallingState, StreamVideoWriteableStateStore } from '../../store';
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
6
6
  import {
7
+ mockBrowserPermission,
7
8
  mockCall,
8
9
  mockDeviceIds$,
9
10
  mockVideoDevices,
@@ -22,6 +23,8 @@ vi.mock('../devices.ts', () => {
22
23
  return of(mockVideoDevices);
23
24
  }),
24
25
  getVideoStream: vi.fn(() => Promise.resolve(mockVideoStream())),
26
+ getAudioBrowserPermission: () => mockBrowserPermission,
27
+ getVideoBrowserPermission: () => mockBrowserPermission,
25
28
  deviceIds$: mockDeviceIds$(),
26
29
  };
27
30
  });
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
  import {
7
7
  MockTrack,
8
8
  emitDeviceIds,
9
+ mockBrowserPermission,
9
10
  mockCall,
10
11
  mockDeviceIds$,
11
12
  mockVideoDevices,
@@ -26,6 +27,8 @@ vi.mock('../../Call.ts', () => {
26
27
  vi.mock('../devices.ts', () => {
27
28
  console.log('MOCKING devices API');
28
29
  return {
30
+ getAudioBrowserPermission: () => mockBrowserPermission,
31
+ getVideoBrowserPermission: () => mockBrowserPermission,
29
32
  deviceIds$: mockDeviceIds$(),
30
33
  };
31
34
  });
@@ -48,7 +51,7 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
48
51
  call,
49
52
  new TestInputMediaDeviceManagerState(
50
53
  'stop-tracks',
51
- 'camera' as PermissionName,
54
+ mockBrowserPermission,
52
55
  ),
53
56
  TrackType.VIDEO,
54
57
  );
@@ -315,8 +318,7 @@ describe('InputMediaDeviceManager.test', () => {
315
318
  await vi.runAllTimersAsync();
316
319
 
317
320
  expect(manager.state.status).toBe('disabled');
318
- expect(manager.disablePromise).toBeUndefined();
319
- expect(manager.enablePromise).toBeUndefined();
321
+ expect(manager.state.optimisticStatus).toBe('disabled');
320
322
  expect(manager.state.selectedDevice).toBe(device.deviceId);
321
323
 
322
324
  vi.useRealTimers();
@@ -5,7 +5,11 @@ import { StreamClient } from '../../coordinator/connection/client';
5
5
  import { StreamVideoWriteableStateStore } from '../../store';
6
6
  import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
7
7
  import { InputMediaDeviceManager } from '../InputMediaDeviceManager';
8
- import { mockVideoDevices, mockVideoStream } from './mocks';
8
+ import {
9
+ mockBrowserPermission,
10
+ mockVideoDevices,
11
+ mockVideoStream,
12
+ } from './mocks';
9
13
  import { TrackType } from '../../gen/video/sfu/models/models';
10
14
 
11
15
  import '../../rtc/__tests__/mocks/webrtc.mocks';
@@ -28,7 +32,7 @@ class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMedia
28
32
  call,
29
33
  new TestInputMediaDeviceManagerState(
30
34
  'stop-tracks',
31
- 'camera' as PermissionName,
35
+ mockBrowserPermission,
32
36
  ),
33
37
  TrackType.VIDEO,
34
38
  );
@@ -1,72 +1,66 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
3
+ import { firstValueFrom } from 'rxjs';
4
+ import { BrowserPermission } from '../BrowserPermission';
3
5
 
4
6
  class TestInputMediaDeviceManagerState extends InputMediaDeviceManagerState {
5
7
  constructor() {
6
- super('stop-tracks', 'camera' as PermissionName);
8
+ super(
9
+ 'stop-tracks',
10
+ new BrowserPermission({
11
+ queryName: 'camera' as PermissionName,
12
+ constraints: {},
13
+ }),
14
+ );
7
15
  }
8
16
 
9
17
  getDeviceIdFromStream = vi.fn();
10
18
  }
11
19
 
12
- describe('InputMediaDeviceManagerState', () => {
13
- let state: InputMediaDeviceManagerState;
14
-
15
- beforeEach(() => {
16
- state = new TestInputMediaDeviceManagerState();
17
- });
20
+ function mockPermissionStatus(state: PermissionState): PermissionStatus {
21
+ return {
22
+ state,
23
+ addEventListener: vi.fn(),
24
+ removeEventListener: vi.fn(),
25
+ } as any;
26
+ }
18
27
 
28
+ describe('InputMediaDeviceManagerState', () => {
19
29
  describe('hasBrowserPermission', () => {
20
30
  it('should emit true when permission is granted', async () => {
21
- const permissionStatus: Partial<PermissionStatus> = {
22
- state: 'granted',
23
- addEventListener: vi.fn(),
24
- };
31
+ const permissionStatus = mockPermissionStatus('granted');
25
32
  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 };
33
+ globalThis.navigator = { permissions: { query } } as any;
34
+ const state = new TestInputMediaDeviceManagerState();
35
+
36
+ const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
29
37
 
30
- const hasPermission = await new Promise((resolve) => {
31
- state.hasBrowserPermission$.subscribe((v) => resolve(v));
32
- });
33
38
  expect(hasPermission).toBe(true);
34
39
  expect(query).toHaveBeenCalledWith({ name: 'camera' });
35
40
  expect(permissionStatus.addEventListener).toHaveBeenCalled();
36
41
  });
37
42
 
38
43
  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 permissionStatus = mockPermissionStatus('denied');
44
45
  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 };
46
+ globalThis.navigator = { permissions: { query } } as any;
47
+ const state = new TestInputMediaDeviceManagerState();
48
+
49
+ const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
48
50
 
49
- const hasPermission = await new Promise((resolve) => {
50
- state.hasBrowserPermission$.subscribe((v) => resolve(v));
51
- });
52
51
  expect(hasPermission).toBe(false);
53
52
  expect(query).toHaveBeenCalledWith({ name: 'camera' });
54
53
  expect(permissionStatus.addEventListener).toHaveBeenCalled();
55
54
  });
56
55
 
57
56
  it('should emit true when prompt is needed', async () => {
58
- const permissionStatus: Partial<PermissionStatus> = {
59
- state: 'prompt',
60
- addEventListener: vi.fn(),
61
- };
57
+ const permissionStatus = mockPermissionStatus('prompt');
62
58
  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 };
59
+ globalThis.navigator = { permissions: { query } } as any;
60
+ const state = new TestInputMediaDeviceManagerState();
61
+
62
+ const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
66
63
 
67
- const hasPermission = await new Promise((resolve) => {
68
- state.hasBrowserPermission$.subscribe((v) => resolve(v));
69
- });
70
64
  expect(hasPermission).toBe(true);
71
65
  expect(query).toHaveBeenCalledWith({ name: 'camera' });
72
66
  expect(permissionStatus.addEventListener).toHaveBeenCalled();
@@ -74,25 +68,21 @@ describe('InputMediaDeviceManagerState', () => {
74
68
 
75
69
  it('should emit true when permissions cannot be queried', async () => {
76
70
  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 };
71
+ globalThis.navigator = { permissions: { query } } as any;
72
+ const state = new TestInputMediaDeviceManagerState();
73
+
74
+ const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
80
75
 
81
- const hasPermission = await new Promise((resolve) => {
82
- state.hasBrowserPermission$.subscribe((v) => resolve(v));
83
- });
84
76
  expect(hasPermission).toBe(true);
85
77
  expect(query).toHaveBeenCalledWith({ name: 'camera' });
86
78
  });
87
79
 
88
80
  it('should emit true when permissions API is unavailable', async () => {
89
- globalThis.navigator ??= {} as Navigator;
90
- // @ts-ignore - navigator is readonly, but we need to mock it
91
- globalThis.navigator.permissions = null;
81
+ globalThis.navigator = {} as any;
82
+ const state = new TestInputMediaDeviceManagerState();
83
+
84
+ const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
92
85
 
93
- const hasPermission = await new Promise((resolve) => {
94
- state.hasBrowserPermission$.subscribe((v) => resolve(v));
95
- });
96
86
  expect(hasPermission).toBe(true);
97
87
  });
98
88
  });
@@ -12,6 +12,7 @@ import { CallingState, StreamVideoWriteableStateStore } from '../../store';
12
12
  import {
13
13
  mockAudioDevices,
14
14
  mockAudioStream,
15
+ mockBrowserPermission,
15
16
  mockCall,
16
17
  mockDeviceIds$,
17
18
  } from './mocks';
@@ -31,6 +32,8 @@ vi.mock('../devices.ts', () => {
31
32
  return of(mockAudioDevices);
32
33
  }),
33
34
  getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
35
+ getAudioBrowserPermission: () => mockBrowserPermission,
36
+ getVideoBrowserPermission: () => mockBrowserPermission,
34
37
  deviceIds$: mockDeviceIds$(),
35
38
  };
36
39
  });
@@ -50,7 +53,7 @@ vi.mock('../../Call.ts', () => {
50
53
  });
51
54
 
52
55
  class NoiseCancellationStub implements INoiseCancellation {
53
- private listeners: { [event: string]: Array<() => void> } = {};
56
+ private listeners: { [event: string]: Array<(arg: boolean) => void> } = {};
54
57
 
55
58
  isSupported = () => true;
56
59
  init = () => Promise.resolve(undefined);
@@ -4,7 +4,12 @@ import { MicrophoneManager } from '../MicrophoneManager';
4
4
  import { Call } from '../../Call';
5
5
  import { StreamClient } from '../../coordinator/connection/client';
6
6
  import { StreamVideoWriteableStateStore } from '../../store';
7
- import { mockAudioDevices, mockAudioStream, mockCall } from './mocks';
7
+ import {
8
+ mockAudioDevices,
9
+ mockAudioStream,
10
+ mockBrowserPermission,
11
+ mockCall,
12
+ } from './mocks';
8
13
  import { of } from 'rxjs';
9
14
  import '../../rtc/__tests__/mocks/webrtc.mocks';
10
15
  import { OwnCapability } from '../../gen/coordinator';
@@ -25,6 +30,8 @@ vi.mock('../devices.ts', () => {
25
30
  return of(mockAudioDevices);
26
31
  }),
27
32
  getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
33
+ getAudioBrowserPermission: () => mockBrowserPermission,
34
+ getVideoBrowserPermission: () => mockBrowserPermission,
28
35
  deviceIds$: {},
29
36
  };
30
37
  });
@@ -1,5 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { emitDeviceIds, mockAudioDevices, mockDeviceIds$ } from './mocks';
2
+ import {
3
+ emitDeviceIds,
4
+ mockAudioDevices,
5
+ mockBrowserPermission,
6
+ mockDeviceIds$,
7
+ } from './mocks';
3
8
  import { of } from 'rxjs';
4
9
  import { SpeakerManager } from '../SpeakerManager';
5
10
  import { checkIfAudioOutputChangeSupported } from '../devices';
@@ -12,6 +17,8 @@ vi.mock('../devices.ts', () => {
12
17
  return {
13
18
  getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
14
19
  checkIfAudioOutputChangeSupported: vi.fn(() => true),
20
+ getAudioBrowserPermission: () => mockBrowserPermission,
21
+ getVideoBrowserPermission: () => mockBrowserPermission,
15
22
  deviceIds$: mockDeviceIds$(),
16
23
  };
17
24
  });
@@ -5,7 +5,8 @@ import {
5
5
  OwnCapability,
6
6
  } from '../../gen/coordinator';
7
7
  import { Call } from '../../Call';
8
- import { Subject } from 'rxjs';
8
+ import { of, Subject } from 'rxjs';
9
+ import { BrowserPermission } from '../BrowserPermission';
9
10
 
10
11
  export const mockVideoDevices = [
11
12
  {
@@ -217,3 +218,7 @@ export const mockDeviceIds$ = () => {
217
218
  export const emitDeviceIds = (values: MediaDeviceInfo[]) => {
218
219
  deviceIds.next(values);
219
220
  };
221
+
222
+ export const mockBrowserPermission = {
223
+ asObservable: () => of(true),
224
+ } as BrowserPermission;