@stream-io/video-client 1.4.2 → 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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.browser.es.js +214 -144
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +214 -142
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +214 -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/helpers/ViewportTracker.d.ts +1 -1
  18. package/dist/src/helpers/lazy.d.ts +4 -0
  19. package/dist/src/helpers/sdp-munging.d.ts +2 -2
  20. package/dist/src/rtc/Dispatcher.d.ts +2 -2
  21. package/dist/src/rtc/codecs.d.ts +1 -1
  22. package/dist/src/rtc/signal.d.ts +0 -1
  23. package/dist/src/stats/utils.d.ts +4 -4
  24. package/dist/src/store/CallState.d.ts +1 -1
  25. package/package.json +4 -4
  26. package/src/devices/BrowserPermission.ts +152 -0
  27. package/src/devices/CameraManagerState.ts +2 -6
  28. package/src/devices/InputMediaDeviceManagerState.ts +10 -44
  29. package/src/devices/MicrophoneManagerState.ts +2 -6
  30. package/src/devices/__tests__/CameraManager.test.ts +3 -0
  31. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +5 -3
  32. package/src/devices/__tests__/InputMediaDeviceManagerFilters.test.ts +6 -2
  33. package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +41 -51
  34. package/src/devices/__tests__/MicrophoneManager.test.ts +4 -1
  35. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +8 -1
  36. package/src/devices/__tests__/SpeakerManager.test.ts +8 -1
  37. package/src/devices/__tests__/mocks.ts +6 -1
  38. package/src/devices/devices.ts +113 -112
  39. package/src/helpers/RNSpeechDetector.ts +1 -1
  40. package/src/helpers/lazy.ts +15 -0
@@ -2,12 +2,15 @@ import {
2
2
  concatMap,
3
3
  debounceTime,
4
4
  from,
5
+ fromEvent,
5
6
  map,
6
7
  merge,
7
- Observable,
8
8
  shareReplay,
9
+ startWith,
9
10
  } from 'rxjs';
10
11
  import { getLogger } from '../logger';
12
+ import { BrowserPermission } from './BrowserPermission';
13
+ import { lazy } from '../helpers/lazy';
11
14
 
12
15
  /**
13
16
  * Returns an Observable that emits the list of available devices
@@ -16,49 +19,28 @@ import { getLogger } from '../logger';
16
19
  * @param constraints the constraints to use when requesting the devices.
17
20
  * @param kind the kind of devices to enumerate.
18
21
  */
19
- const getDevices = (
20
- constraints: MediaStreamConstraints,
21
- kind: MediaDeviceKind,
22
- ) => {
23
- return new Observable<MediaDeviceInfo[]>((subscriber) => {
24
- const enumerate = async () => {
22
+ const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
23
+ return from(
24
+ (async () => {
25
25
  let devices = await navigator.mediaDevices.enumerateDevices();
26
- // some browsers report empty device labels (Firefox).
27
- // in that case, we need to request permissions (via getUserMedia)
28
- // to be able to get the device labels
29
- const needsGetUserMedia = devices.some(
26
+ // for privacy reasons, most browsers don't give you device labels
27
+ // unless you have a corresponding camera or microphone permission
28
+ const shouldPromptForBrowserPermission = devices.some(
30
29
  (device) => device.kind === kind && device.label === '',
31
30
  );
32
- if (needsGetUserMedia) {
33
- let mediaStream: MediaStream | undefined;
34
- try {
35
- mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
36
- devices = await navigator.mediaDevices.enumerateDevices();
37
- } finally {
38
- if (mediaStream) disposeOfMediaStream(mediaStream);
39
- }
31
+ if (shouldPromptForBrowserPermission) {
32
+ await permission.prompt({ throwOnNotAllowed: true });
33
+ devices = await navigator.mediaDevices.enumerateDevices();
40
34
  }
41
- return devices;
42
- };
43
-
44
- enumerate()
45
- .then((devices) => {
46
- // notify subscribers and complete
47
- subscriber.next(devices);
48
- subscriber.complete();
49
- })
50
- .catch((error) => {
51
- const logger = getLogger(['devices']);
52
- logger('error', 'Failed to enumerate devices', error);
53
- subscriber.error(error);
54
- });
55
- });
35
+ return devices.filter((d) => d.kind === kind);
36
+ })(),
37
+ );
56
38
  };
57
39
 
58
40
  /**
59
- * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
60
- *
61
- * */
41
+ * Tells if the browser supports audio output change on 'audio' elements,
42
+ * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId.
43
+ */
62
44
  export const checkIfAudioOutputChangeSupported = () => {
63
45
  if (typeof document === 'undefined') return false;
64
46
  const element = document.createElement('audio');
@@ -87,97 +69,92 @@ const videoDeviceConstraints = {
87
69
  } satisfies MediaStreamConstraints;
88
70
 
89
71
  /**
90
- * Creates a memoized observable instance
91
- * that will be created only once and shared between all callers.
92
- *
93
- * @param create a function that creates an Observable.
72
+ * Keeps track of the browser permission to use microphone. This permission also
73
+ * affects an ability to enumerate audio devices.
94
74
  */
95
- const memoizedObservable = <T>(create: () => Observable<T>) => {
96
- let memoized: Observable<T>;
97
- return () => {
98
- if (!memoized) memoized = create();
99
- return memoized;
100
- };
101
- };
75
+ export const getAudioBrowserPermission = lazy(
76
+ () =>
77
+ new BrowserPermission({
78
+ constraints: audioDeviceConstraints,
79
+ queryName: 'microphone' as PermissionName,
80
+ }),
81
+ );
102
82
 
103
- const getDeviceChangeObserver = memoizedObservable(() => {
104
- // Audio and video devices are requested in two separate requests.
105
- // That way, users will be presented with two separate prompts
106
- // -> they can give access to just camera, or just microphone
107
- return new Observable((subscriber) => {
108
- // 'addEventListener' is not available in React Native
109
- if (!navigator.mediaDevices.addEventListener) return;
83
+ /**
84
+ * Keeps track of the browser permission to use camera. This permission also
85
+ * affects an ability to enumerate video devices.
86
+ */
87
+ export const getVideoBrowserPermission = lazy(
88
+ () =>
89
+ new BrowserPermission({
90
+ constraints: videoDeviceConstraints,
91
+ queryName: 'camera' as PermissionName,
92
+ }),
93
+ );
110
94
 
111
- const notify = () => subscriber.next();
112
- navigator.mediaDevices.addEventListener('devicechange', notify);
113
- return () => {
114
- navigator.mediaDevices.removeEventListener('devicechange', notify);
115
- };
116
- }).pipe(
95
+ const getDeviceChangeObserver = lazy(() => {
96
+ // 'addEventListener' is not available in React Native, returning
97
+ // an observable that will never fire
98
+ if (!navigator.mediaDevices.addEventListener) return from([]);
99
+ return fromEvent(navigator.mediaDevices, 'devicechange').pipe(
100
+ map(() => undefined),
117
101
  debounceTime(500),
118
- concatMap(() => from(navigator.mediaDevices.enumerateDevices())),
119
- shareReplay(1),
120
102
  );
121
103
  });
122
104
 
123
- const getAudioDevicesObserver = memoizedObservable(() => {
124
- return merge(
125
- getDevices(audioDeviceConstraints, 'audioinput'),
126
- getDeviceChangeObserver(),
127
- ).pipe(shareReplay(1));
128
- });
129
-
130
- const getAudioOutputDevicesObserver = memoizedObservable(() => {
131
- return merge(
132
- getDevices(audioDeviceConstraints, 'audiooutput'),
133
- getDeviceChangeObserver(),
134
- ).pipe(shareReplay(1));
135
- });
136
-
137
- const getVideoDevicesObserver = memoizedObservable(() => {
138
- return merge(
139
- getDevices(videoDeviceConstraints, 'videoinput'),
140
- getDeviceChangeObserver(),
141
- ).pipe(shareReplay(1));
142
- });
143
-
144
105
  /**
145
- * 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.
106
+ * Prompts the user for a permission to use audio devices (if not already granted
107
+ * and was not prompted before) and lists the available 'audioinput' devices,
108
+ * if devices are added/removed the list is updated, and if the permission is revoked,
109
+ * the observable errors.
146
110
  */
147
- export const getAudioDevices = () => {
148
- return getAudioDevicesObserver().pipe(
149
- map((values) => values.filter((d) => d.kind === 'audioinput')),
111
+ export const getAudioDevices = lazy(() => {
112
+ return merge(
113
+ getDeviceChangeObserver(),
114
+ getAudioBrowserPermission().asObservable(),
115
+ ).pipe(
116
+ startWith(undefined),
117
+ concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput')),
118
+ shareReplay(1),
150
119
  );
151
- };
120
+ });
152
121
 
153
122
  /**
154
- * Prompts the user for a permission to use video devices (if not already granted) and lists the available 'videoinput' devices, if devices are added/removed the list is updated.
123
+ * Prompts the user for a permission to use video devices (if not already granted
124
+ * and was not prompted before) and lists the available 'videoinput' devices,
125
+ * if devices are added/removed the list is updated, and if the permission is revoked,
126
+ * the observable errors.
155
127
  */
156
128
  export const getVideoDevices = () => {
157
- return getVideoDevicesObserver().pipe(
158
- map((values) => values.filter((d) => d.kind === 'videoinput')),
129
+ return merge(
130
+ getDeviceChangeObserver(),
131
+ getVideoBrowserPermission().asObservable(),
132
+ ).pipe(
133
+ startWith(undefined),
134
+ concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput')),
135
+ shareReplay(1),
159
136
  );
160
137
  };
161
138
 
162
139
  /**
163
- * Prompts the user for a permission to use audio devices (if not already granted) and lists the available 'audiooutput' devices, if devices are added/removed the list is updated. Selecting 'audiooutput' device only makes sense if [the browser has support for changing audio output on 'audio' elements](#checkifaudiooutputchangesupported)
140
+ * Prompts the user for a permission to use video devices (if not already granted
141
+ * and was not prompted before) and lists the available 'audiooutput' devices,
142
+ * if devices are added/removed the list is updated, and if the permission is revoked,
143
+ * the observable errors.
164
144
  */
165
145
  export const getAudioOutputDevices = () => {
166
- return getAudioOutputDevicesObserver().pipe(
167
- map((values) => values.filter((d) => d.kind === 'audiooutput')),
146
+ return merge(
147
+ getDeviceChangeObserver(),
148
+ getAudioBrowserPermission().asObservable(),
149
+ ).pipe(
150
+ startWith(undefined),
151
+ concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')),
152
+ shareReplay(1),
168
153
  );
169
154
  };
170
155
 
171
156
  const getStream = async (constraints: MediaStreamConstraints) => {
172
- try {
173
- return await navigator.mediaDevices.getUserMedia(constraints);
174
- } catch (e) {
175
- getLogger(['devices'])('error', `Failed to getUserMedia`, {
176
- error: e,
177
- constraints: constraints,
178
- });
179
- throw e;
180
- }
157
+ return await navigator.mediaDevices.getUserMedia(constraints);
181
158
  };
182
159
 
183
160
  /**
@@ -197,7 +174,20 @@ export const getAudioStream = async (
197
174
  ...trackConstraints,
198
175
  },
199
176
  };
200
- return getStream(constraints);
177
+
178
+ try {
179
+ await getAudioBrowserPermission().prompt({
180
+ throwOnNotAllowed: true,
181
+ forcePrompt: true,
182
+ });
183
+ return getStream(constraints);
184
+ } catch (e) {
185
+ getLogger(['devices'])('error', 'Failed to get audio stream', {
186
+ error: e,
187
+ constraints: constraints,
188
+ });
189
+ throw e;
190
+ }
201
191
  };
202
192
 
203
193
  /**
@@ -217,7 +207,19 @@ export const getVideoStream = async (
217
207
  ...trackConstraints,
218
208
  },
219
209
  };
220
- return getStream(constraints);
210
+ try {
211
+ await getVideoBrowserPermission().prompt({
212
+ throwOnNotAllowed: true,
213
+ forcePrompt: true,
214
+ });
215
+ return getStream(constraints);
216
+ } catch (e) {
217
+ getLogger(['devices'])('error', 'Failed to get video stream', {
218
+ error: e,
219
+ constraints: constraints,
220
+ });
221
+ throw e;
222
+ }
221
223
  };
222
224
 
223
225
  /**
@@ -257,12 +259,11 @@ export const getScreenShareStream = async (
257
259
  export const deviceIds$ =
258
260
  typeof navigator !== 'undefined' &&
259
261
  typeof navigator.mediaDevices !== 'undefined'
260
- ? memoizedObservable(() =>
261
- merge(
262
- from(navigator.mediaDevices.enumerateDevices()),
263
- getDeviceChangeObserver(),
264
- ).pipe(shareReplay(1)),
265
- )()
262
+ ? getDeviceChangeObserver().pipe(
263
+ startWith(undefined),
264
+ concatMap(() => navigator.mediaDevices.enumerateDevices()),
265
+ shareReplay(1),
266
+ )
266
267
  : undefined;
267
268
 
268
269
  /**
@@ -7,7 +7,7 @@ const AUDIO_LEVEL_THRESHOLD = 0.2;
7
7
  export class RNSpeechDetector {
8
8
  private pc1 = new RTCPeerConnection({});
9
9
  private pc2 = new RTCPeerConnection({});
10
- private intervalId: NodeJS.Timer | undefined;
10
+ private intervalId: NodeJS.Timeout | undefined;
11
11
 
12
12
  /**
13
13
  * Starts the speech detection.
@@ -0,0 +1,15 @@
1
+ const uninitialized = Symbol('uninitialized');
2
+
3
+ /**
4
+ * Lazily creates a value using a provided factory
5
+ */
6
+ export function lazy<T>(factory: () => T): () => T {
7
+ let value: T | typeof uninitialized = uninitialized;
8
+ return () => {
9
+ if (value === uninitialized) {
10
+ value = factory();
11
+ }
12
+
13
+ return value;
14
+ };
15
+ }