@stream-io/video-client 0.2.3 → 0.3.1

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 (71) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.browser.es.js +982 -675
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +984 -673
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +982 -675
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +21 -9
  9. package/dist/src/StreamVideoClient.d.ts +3 -1
  10. package/dist/src/devices/CameraManager.d.ts +31 -0
  11. package/dist/src/devices/CameraManagerState.d.ts +28 -0
  12. package/dist/src/devices/InputMediaDeviceManager.d.ts +47 -0
  13. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +69 -0
  14. package/dist/src/devices/MicrophoneManager.d.ts +19 -0
  15. package/dist/src/devices/MicrophoneManagerState.d.ts +4 -0
  16. package/dist/src/devices/__tests__/mocks.d.ts +13 -0
  17. package/dist/src/devices/index.d.ts +4 -0
  18. package/dist/src/events/call-permissions.d.ts +0 -5
  19. package/dist/src/events/call.d.ts +0 -6
  20. package/dist/src/events/index.d.ts +0 -6
  21. package/dist/src/rtc/Dispatcher.d.ts +2 -2
  22. package/dist/src/rtc/Publisher.d.ts +0 -1
  23. package/dist/src/store/CallState.d.ts +164 -89
  24. package/dist/src/types.d.ts +5 -7
  25. package/dist/version.d.ts +1 -1
  26. package/package.json +1 -1
  27. package/src/Call.ts +130 -44
  28. package/src/StreamVideoClient.ts +14 -17
  29. package/src/__tests__/StreamVideoClient.test.ts +3 -0
  30. package/src/devices/CameraManager.ts +73 -0
  31. package/src/devices/CameraManagerState.ts +61 -0
  32. package/src/devices/InputMediaDeviceManager.ts +121 -0
  33. package/src/devices/InputMediaDeviceManagerState.ts +111 -0
  34. package/src/devices/MicrophoneManager.ts +45 -0
  35. package/src/devices/MicrophoneManagerState.ts +9 -0
  36. package/src/devices/__tests__/CameraManager.test.ts +150 -0
  37. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +159 -0
  38. package/src/devices/__tests__/MicrophoneManager.test.ts +103 -0
  39. package/src/devices/__tests__/mocks.ts +98 -0
  40. package/src/devices/index.ts +4 -0
  41. package/src/events/__tests__/call-permissions.test.ts +1 -61
  42. package/src/events/__tests__/call.test.ts +5 -50
  43. package/src/events/call-permissions.ts +0 -14
  44. package/src/events/call.ts +5 -16
  45. package/src/events/callEventHandlers.ts +2 -57
  46. package/src/events/index.ts +0 -6
  47. package/src/rtc/Dispatcher.ts +2 -2
  48. package/src/rtc/Publisher.ts +4 -6
  49. package/src/store/CallState.ts +475 -119
  50. package/src/store/__tests__/CallState.test.ts +447 -1
  51. package/src/types.ts +4 -8
  52. package/dist/src/events/__tests__/sessions.test.d.ts +0 -1
  53. package/dist/src/events/backstage.d.ts +0 -6
  54. package/dist/src/events/members.d.ts +0 -18
  55. package/dist/src/events/moderation.d.ts +0 -14
  56. package/dist/src/events/reactions.d.ts +0 -8
  57. package/dist/src/events/recording.d.ts +0 -18
  58. package/dist/src/events/sessions.d.ts +0 -26
  59. package/src/events/__tests__/backstage.test.ts +0 -15
  60. package/src/events/__tests__/members.test.ts +0 -135
  61. package/src/events/__tests__/recording.test.ts +0 -65
  62. package/src/events/__tests__/sessions.test.ts +0 -135
  63. package/src/events/backstage.ts +0 -15
  64. package/src/events/members.ts +0 -62
  65. package/src/events/moderation.ts +0 -35
  66. package/src/events/reactions.ts +0 -30
  67. package/src/events/recording.ts +0 -64
  68. package/src/events/sessions.ts +0 -102
  69. /package/dist/src/{events/__tests__/backstage.test.d.ts → devices/__tests__/CameraManager.test.d.ts} +0 -0
  70. /package/dist/src/{events/__tests__/members.test.d.ts → devices/__tests__/InputMediaDeviceManager.test.d.ts} +0 -0
  71. /package/dist/src/{events/__tests__/recording.test.d.ts → devices/__tests__/MicrophoneManager.test.d.ts} +0 -0
package/src/Call.ts CHANGED
@@ -9,7 +9,11 @@ import {
9
9
  Subscriber,
10
10
  } from './rtc';
11
11
  import { muteTypeToTrackType } from './rtc/helpers/tracks';
12
- import { GoAwayReason, TrackType } from './gen/video/sfu/models/models';
12
+ import {
13
+ GoAwayReason,
14
+ SdkType,
15
+ TrackType,
16
+ } from './gen/video/sfu/models/models';
13
17
  import {
14
18
  registerEventHandlers,
15
19
  registerRingingCallEventHandlers,
@@ -105,11 +109,15 @@ import {
105
109
  CallEventHandler,
106
110
  CallEventTypes,
107
111
  EventHandler,
112
+ EventTypes,
108
113
  Logger,
109
114
  StreamCallEvent,
110
115
  } from './coordinator/connection/types';
111
- import { getClientDetails } from './client-details';
116
+ import { getClientDetails, getSdkInfo } from './client-details';
112
117
  import { getLogger } from './logger';
118
+ import { CameraManager } from './devices/CameraManager';
119
+ import { MicrophoneManager } from './devices/MicrophoneManager';
120
+ import { CameraDirection } from './devices/CameraManagerState';
113
121
 
114
122
  /**
115
123
  * An object representation of a `Call`.
@@ -146,6 +154,16 @@ export class Call {
146
154
  */
147
155
  watching: boolean;
148
156
 
157
+ /**
158
+ * Device manager for the camera
159
+ */
160
+ readonly camera: CameraManager;
161
+
162
+ /**
163
+ * Device manager for the microhpone
164
+ */
165
+ readonly microphone: MicrophoneManager;
166
+
149
167
  /**
150
168
  * Flag telling whether this call is a "ringing" call.
151
169
  */
@@ -174,7 +192,7 @@ export class Call {
174
192
  private dropTimeout: ReturnType<typeof setTimeout> | undefined;
175
193
 
176
194
  private readonly clientStore: StreamVideoWriteableStateStore;
177
- private readonly streamClient: StreamClient;
195
+ public readonly streamClient: StreamClient;
178
196
  private sfuClient?: StreamSfuClient;
179
197
  private reconnectAttempts = 0;
180
198
  private maxReconnectAttempts = 10;
@@ -200,7 +218,6 @@ export class Call {
200
218
  type,
201
219
  id,
202
220
  streamClient,
203
- metadata,
204
221
  members,
205
222
  ownCapabilities,
206
223
  sortParticipantsBy,
@@ -225,13 +242,17 @@ export class Call {
225
242
  this.state.setSortParticipantsBy(participantSorter);
226
243
  }
227
244
 
228
- this.state.setMetadata(metadata);
229
245
  this.state.setMembers(members || []);
230
246
  this.state.setOwnCapabilities(ownCapabilities || []);
231
247
  this.state.setCallingState(
232
248
  ringing ? CallingState.RINGING : CallingState.IDLE,
233
249
  );
234
250
 
251
+ this.on('all', (event) => {
252
+ // update state with the latest event data
253
+ this.state.updateFromEvent(event);
254
+ });
255
+
235
256
  this.leaveCallHooks.push(
236
257
  registerEventHandlers(this, this.state, this.dispatcher),
237
258
  );
@@ -246,14 +267,17 @@ export class Call {
246
267
  (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions),
247
268
  ),
248
269
  );
270
+
271
+ this.camera = new CameraManager(this);
272
+ this.microphone = new MicrophoneManager(this);
249
273
  }
250
274
 
251
275
  private registerEffects() {
252
276
  this.leaveCallHooks.push(
253
- // handles updating the permissions context when the metadata changes.
254
- createSubscription(this.state.metadata$, (metadata) => {
255
- if (!metadata) return;
256
- this.permissionsContext.setCallSettings(metadata.settings);
277
+ // handles updating the permissions context when the settings change.
278
+ createSubscription(this.state.settings$, (settings) => {
279
+ if (!settings) return;
280
+ this.permissionsContext.setCallSettings(settings);
257
281
  }),
258
282
 
259
283
  // handle the case when the user permissions are modified.
@@ -284,13 +308,10 @@ export class Call {
284
308
  }),
285
309
 
286
310
  // handles the case when the user is blocked by the call owner.
287
- createSubscription(this.state.metadata$, async (metadata) => {
288
- if (!metadata) return;
311
+ createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
312
+ if (!blockedUserIds) return;
289
313
  const currentUserId = this.currentUserId;
290
- if (
291
- currentUserId &&
292
- metadata.blocked_user_ids.includes(currentUserId)
293
- ) {
314
+ if (currentUserId && blockedUserIds.includes(currentUserId)) {
294
315
  this.logger('info', 'Leaving call because of being blocked');
295
316
  await this.leave();
296
317
  }
@@ -329,9 +350,9 @@ export class Call {
329
350
  * @returns a function which can be called to unsubscribe from the given event(s)
330
351
  */
331
352
  on(eventName: SfuEventKinds, fn: SfuEventListener): () => void;
332
- on(eventName: CallEventTypes, fn: CallEventHandler): () => void;
353
+ on(eventName: EventTypes, fn: CallEventHandler): () => void;
333
354
  on(
334
- eventName: SfuEventKinds | CallEventTypes,
355
+ eventName: SfuEventKinds | EventTypes,
335
356
  fn: SfuEventListener | CallEventHandler,
336
357
  ) {
337
358
  if (isSfuEvent(eventName)) {
@@ -420,13 +441,6 @@ export class Call {
420
441
  this.state.setCallingState(CallingState.LEFT);
421
442
  };
422
443
 
423
- /**
424
- * A getter for the call metadata.
425
- */
426
- get data() {
427
- return this.state.metadata;
428
- }
429
-
430
444
  /**
431
445
  * A flag indicating whether the call is "ringing" type of call.
432
446
  */
@@ -445,7 +459,7 @@ export class Call {
445
459
  * A flag indicating whether the call was created by the current user.
446
460
  */
447
461
  get isCreatedByMe() {
448
- return this.state.metadata?.created_by.id === this.currentUserId;
462
+ return this.state.createdBy?.id === this.currentUserId;
449
463
  }
450
464
 
451
465
  /**
@@ -469,7 +483,7 @@ export class Call {
469
483
  this.ringingSubject.next(true);
470
484
  }
471
485
 
472
- this.state.setMetadata(response.call);
486
+ this.state.updateFromCallResponse(response.call);
473
487
  this.state.setMembers(response.members);
474
488
  this.state.setOwnCapabilities(response.own_capabilities);
475
489
 
@@ -496,7 +510,7 @@ export class Call {
496
510
  this.ringingSubject.next(true);
497
511
  }
498
512
 
499
- this.state.setMetadata(response.call);
513
+ this.state.updateFromCallResponse(response.call);
500
514
  this.state.setMembers(response.members);
501
515
  this.state.setOwnCapabilities(response.own_capabilities);
502
516
 
@@ -598,7 +612,7 @@ export class Call {
598
612
  let connectionConfig: RTCConfiguration | undefined;
599
613
  try {
600
614
  const call = await join(this.streamClient, this.type, this.id, data);
601
- this.state.setMetadata(call.metadata);
615
+ this.state.updateFromCallResponse(call.metadata);
602
616
  this.state.setMembers(call.members);
603
617
  this.state.setOwnCapabilities(call.ownCapabilities);
604
618
  connectionConfig = call.connectionConfig;
@@ -815,7 +829,7 @@ export class Call {
815
829
  });
816
830
  }
817
831
 
818
- const audioSettings = this.data?.settings.audio;
832
+ const audioSettings = this.state.settings?.audio;
819
833
  const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
820
834
  const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
821
835
 
@@ -910,6 +924,12 @@ export class Call {
910
924
  this.reconnectAttempts = 0; // reset the reconnect attempts counter
911
925
  this.state.setCallingState(CallingState.JOINED);
912
926
 
927
+ // React uses a different device management for now
928
+ if (getSdkInfo()?.type !== SdkType.REACT) {
929
+ this.initCamera();
930
+ this.initMic();
931
+ }
932
+
913
933
  // 3. once we have the "joinResponse", and possibly reconciled the local state
914
934
  // we schedule a fast subscription update for all remote participants
915
935
  // that were visible before we reconnected or migrated to a new SFU.
@@ -1196,6 +1216,8 @@ export class Call {
1196
1216
  *
1197
1217
  *
1198
1218
  * @param deviceId the selected device, pass `undefined` to clear the device selection
1219
+ *
1220
+ * @deprecated use call.microphone.select
1199
1221
  */
1200
1222
  setAudioDevice = (deviceId?: string) => {
1201
1223
  if (!this.sfuClient) return;
@@ -1210,6 +1232,8 @@ export class Call {
1210
1232
  * This method only stores the selection, if you want to start publishing a media stream call the [`publishVideoStream` method](#publishvideostream) that will set `videoDeviceId` as well.
1211
1233
  *
1212
1234
  * @param deviceId the selected device, pass `undefined` to clear the device selection
1235
+ *
1236
+ * @deprecated use call.camera.select
1213
1237
  */
1214
1238
  setVideoDevice = (deviceId?: string) => {
1215
1239
  if (!this.sfuClient) return;
@@ -1516,7 +1540,7 @@ export class Call {
1516
1540
  >(`${this.streamClientBasePath}`, updates);
1517
1541
 
1518
1542
  const { call, members, own_capabilities } = response;
1519
- this.state.setMetadata(call);
1543
+ this.state.updateFromCallResponse(call);
1520
1544
  this.state.setMembers(members);
1521
1545
  this.state.setOwnCapabilities(own_capabilities);
1522
1546
 
@@ -1617,23 +1641,23 @@ export class Call {
1617
1641
 
1618
1642
  private scheduleAutoDrop = () => {
1619
1643
  if (this.dropTimeout) clearTimeout(this.dropTimeout);
1620
- const subscription = this.state.metadata$
1644
+ const subscription = this.state.settings$
1621
1645
  .pipe(
1622
1646
  pairwise(),
1623
- tap(([prevMeta, currentMeta]) => {
1624
- if (!(currentMeta && this.clientStore.connectedUser)) return;
1647
+ tap(([prevSettings, currentSettings]) => {
1648
+ if (!currentSettings || !this.clientStore.connectedUser) return;
1625
1649
 
1626
1650
  const isOutgoingCall =
1627
- this.currentUserId === currentMeta.created_by.id;
1651
+ this.currentUserId === this.state.createdBy?.id;
1628
1652
 
1629
1653
  const [prevTimeoutMs, timeoutMs] = isOutgoingCall
1630
1654
  ? [
1631
- prevMeta?.settings.ring.auto_cancel_timeout_ms,
1632
- currentMeta.settings.ring.auto_cancel_timeout_ms,
1655
+ prevSettings?.ring.auto_cancel_timeout_ms,
1656
+ currentSettings.ring.auto_cancel_timeout_ms,
1633
1657
  ]
1634
1658
  : [
1635
- prevMeta?.settings.ring.incoming_call_timeout_ms,
1636
- currentMeta.settings.ring.incoming_call_timeout_ms,
1659
+ prevSettings?.ring.incoming_call_timeout_ms,
1660
+ currentSettings.ring.incoming_call_timeout_ms,
1637
1661
  ];
1638
1662
  if (
1639
1663
  typeof timeoutMs === 'undefined' ||
@@ -1658,7 +1682,6 @@ export class Call {
1658
1682
 
1659
1683
  /**
1660
1684
  * Retrieves the list of recordings for the current call or call session.
1661
- * Updates the call state with the returned array of CallRecording objects.
1662
1685
  *
1663
1686
  * If `callSessionId` is provided, it will return the recordings for that call session.
1664
1687
  * Otherwise, all recordings for the current call will be returned.
@@ -1672,13 +1695,9 @@ export class Call {
1672
1695
  if (callSessionId) {
1673
1696
  endpoint = `${endpoint}/${callSessionId}`;
1674
1697
  }
1675
- const response = await this.streamClient.get<ListRecordingsResponse>(
1698
+ return this.streamClient.get<ListRecordingsResponse>(
1676
1699
  `${endpoint}/recordings`,
1677
1700
  );
1678
-
1679
- this.state.setCallRecordingsList(response.recordings);
1680
-
1681
- return response;
1682
1701
  };
1683
1702
 
1684
1703
  /**
@@ -1692,4 +1711,71 @@ export class Call {
1692
1711
  { custom: payload },
1693
1712
  );
1694
1713
  };
1714
+
1715
+ private initCamera() {
1716
+ if (
1717
+ this.state.localParticipant?.videoStream ||
1718
+ !this.permissionsContext.hasPermission('send-video')
1719
+ ) {
1720
+ return;
1721
+ }
1722
+
1723
+ // Set camera direction if it's not yet set
1724
+ // This will also start publishing if camera is enabled
1725
+ if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
1726
+ let defaultDirection: CameraDirection = 'front';
1727
+ const backendSetting = this.state.settings?.video.camera_facing;
1728
+ if (backendSetting) {
1729
+ defaultDirection = backendSetting === 'front' ? 'front' : 'back';
1730
+ }
1731
+ this.camera.selectDirection(defaultDirection);
1732
+ } else if (this.camera.state.status === 'enabled') {
1733
+ // Publish already started media streams (this is the case if there is a lobby screen before join)
1734
+ // Wait for media stream
1735
+ this.camera.state.mediaStream$
1736
+ .pipe(takeWhile((s) => s === undefined, true))
1737
+ .subscribe((stream) => {
1738
+ if (!this.state.localParticipant?.videoStream) {
1739
+ this.publishVideoStream(stream!);
1740
+ }
1741
+ });
1742
+ }
1743
+
1744
+ // Apply backend config (this is the case if there is no lobby screen before join)
1745
+ if (
1746
+ this.camera.state.status === undefined &&
1747
+ this.state.settings?.video.camera_default_on
1748
+ ) {
1749
+ void this.camera.enable();
1750
+ }
1751
+ }
1752
+
1753
+ private initMic() {
1754
+ if (
1755
+ this.state.localParticipant?.audioStream ||
1756
+ !this.permissionsContext.hasPermission('send-audio')
1757
+ ) {
1758
+ return;
1759
+ }
1760
+
1761
+ // Publish already started media streams (this is the case if there is a lobby screen before join)
1762
+ if (this.microphone.state.status === 'enabled') {
1763
+ // Wait for media stream
1764
+ this.microphone.state.mediaStream$
1765
+ .pipe(takeWhile((s) => s === undefined, true))
1766
+ .subscribe((stream) => {
1767
+ if (!this.state.localParticipant?.audioStream) {
1768
+ this.publishAudioStream(stream!);
1769
+ }
1770
+ });
1771
+ }
1772
+
1773
+ // Apply backend config (this is the case if there is no lobby screen before join)
1774
+ if (
1775
+ this.microphone.state.status === undefined &&
1776
+ this.state.settings?.audio.mic_default_on
1777
+ ) {
1778
+ void this.microphone.enable();
1779
+ }
1780
+ }
1695
1781
  }
@@ -217,16 +217,15 @@ export class StreamVideoClient {
217
217
  }
218
218
 
219
219
  this.logger('info', `New call created and registered: ${call.cid}`);
220
- this.writeableStateStore.registerCall(
221
- new Call({
222
- streamClient: this.streamClient,
223
- type: call.type,
224
- id: call.id,
225
- metadata: call,
226
- members,
227
- clientStore: this.writeableStateStore,
228
- }),
229
- );
220
+ const newCall = new Call({
221
+ streamClient: this.streamClient,
222
+ type: call.type,
223
+ id: call.id,
224
+ members,
225
+ clientStore: this.writeableStateStore,
226
+ });
227
+ newCall.state.updateFromCallResponse(call);
228
+ this.writeableStateStore.registerCall(newCall);
230
229
  }),
231
230
  );
232
231
 
@@ -246,7 +245,6 @@ export class StreamVideoClient {
246
245
  // if `call.created` was received before `call.ring`.
247
246
  // In that case, we cleanup the already tracked call.
248
247
  const prevCall = this.writeableStateStore.findCall(call.type, call.id);
249
- const prevMetadata = prevCall?.state.metadata;
250
248
  await prevCall?.leave();
251
249
  // we create a new call
252
250
  const theCall = new Call({
@@ -256,8 +254,8 @@ export class StreamVideoClient {
256
254
  members,
257
255
  clientStore: this.writeableStateStore,
258
256
  ringing: true,
259
- metadata: prevMetadata,
260
257
  });
258
+ theCall.state.updateFromCallResponse(call);
261
259
  // we fetch the latest metadata for the call from the server
262
260
  await theCall.get();
263
261
  this.writeableStateStore.registerCall(theCall);
@@ -357,12 +355,12 @@ export class StreamVideoClient {
357
355
  streamClient: this.streamClient,
358
356
  id: c.call.id,
359
357
  type: c.call.type,
360
- metadata: c.call,
361
358
  members: c.members,
362
359
  ownCapabilities: c.own_capabilities,
363
360
  watching: data.watch,
364
361
  clientStore: this.writeableStateStore,
365
362
  });
363
+ call.state.updateFromCallResponse(c.call);
366
364
  if (data.watch) {
367
365
  this.writeableStateStore.registerCall(call);
368
366
  }
@@ -374,10 +372,9 @@ export class StreamVideoClient {
374
372
  };
375
373
  };
376
374
 
377
- queryUsers = async () => {
378
- console.log('Querying users is not implemented yet.');
379
- };
380
-
375
+ /**
376
+ * Returns a list of available data centers available for hosting calls.
377
+ */
381
378
  edges = async () => {
382
379
  return this.streamClient.get<GetEdgesResponse>(`/edges`);
383
380
  };
@@ -7,6 +7,9 @@ import { StreamVideoServerClient } from '../StreamVideoServerClient';
7
7
  const apiKey = process.env.STREAM_API_KEY!;
8
8
  const secret = process.env.STREAM_SECRET!;
9
9
 
10
+ vi.mock('../devices/CameraManager.ts');
11
+ vi.mock('../devices/MicrophoneManager.ts');
12
+
10
13
  const tokenProvider = (userId: string) => {
11
14
  const serverClient = new StreamVideoServerClient(apiKey, { secret });
12
15
  return async () => {
@@ -0,0 +1,73 @@
1
+ import { Observable } from 'rxjs';
2
+ import { Call } from '../Call';
3
+ import { CameraDirection, CameraManagerState } from './CameraManagerState';
4
+ import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
+ import { getVideoDevices, getVideoStream } from './devices';
6
+ import { TrackType } from '../gen/video/sfu/models/models';
7
+
8
+ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
9
+ constructor(call: Call) {
10
+ super(call, new CameraManagerState());
11
+ }
12
+
13
+ /**
14
+ * Select the camera direaction
15
+ * @param direction
16
+ */
17
+ async selectDirection(direction: Exclude<CameraDirection, undefined>) {
18
+ this.state.setDirection(direction);
19
+ // Providing both device id and direction doesn't work, so we deselect the device
20
+ this.state.setDevice(undefined);
21
+ await this.applySettingsToStream();
22
+ }
23
+
24
+ /**
25
+ * Flips the camera direction: if it's front it will change to back, if it's back, it will change to front.
26
+ *
27
+ * Note: if there is no available camera with the desired direction, this method will do nothing.
28
+ * @returns
29
+ */
30
+ async flip() {
31
+ const newDirection = this.state.direction === 'front' ? 'back' : 'front';
32
+ this.selectDirection(newDirection);
33
+ }
34
+
35
+ protected getDevices(): Observable<MediaDeviceInfo[]> {
36
+ return getVideoDevices();
37
+ }
38
+ protected getStream(
39
+ constraints: MediaTrackConstraints,
40
+ ): Promise<MediaStream> {
41
+ // We can't set both device id and facing mode
42
+ // Device id has higher priority
43
+ if (!constraints.deviceId && this.state.direction) {
44
+ constraints.facingMode =
45
+ this.state.direction === 'front' ? 'user' : 'environment';
46
+ }
47
+ return getVideoStream(constraints);
48
+ }
49
+ protected publishStream(stream: MediaStream): Promise<void> {
50
+ return this.call.publishVideoStream(stream);
51
+ }
52
+ protected stopPublishStream(): Promise<void> {
53
+ return this.call.stopPublish(TrackType.VIDEO);
54
+ }
55
+
56
+ /**
57
+ * Disables the video tracks of the camera
58
+ */
59
+ pause() {
60
+ this.state.mediaStream?.getVideoTracks().forEach((track) => {
61
+ track.enabled = false;
62
+ });
63
+ }
64
+
65
+ /**
66
+ * (Re)enables the video tracks of the camera
67
+ */
68
+ resume() {
69
+ this.state.mediaStream?.getVideoTracks().forEach((track) => {
70
+ track.enabled = true;
71
+ });
72
+ }
73
+ }
@@ -0,0 +1,61 @@
1
+ import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
2
+ import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
3
+ import { isReactNative } from '../helpers/platforms';
4
+
5
+ export type CameraDirection = 'front' | 'back' | undefined;
6
+
7
+ export class CameraManagerState extends InputMediaDeviceManagerState {
8
+ private directionSubject = new BehaviorSubject<CameraDirection>(undefined);
9
+
10
+ /**
11
+ * Observable that emits the preferred camera direction
12
+ * front - means the camera facing the user
13
+ * back - means the camera facing the environment
14
+ */
15
+ direction$: Observable<CameraDirection>;
16
+
17
+ constructor() {
18
+ super();
19
+ this.direction$ = this.directionSubject
20
+ .asObservable()
21
+ .pipe(distinctUntilChanged());
22
+ }
23
+
24
+ /**
25
+ * The preferred camera direction
26
+ * front - means the camera facing the user
27
+ * back - means the camera facing the environment
28
+ */
29
+ get direction() {
30
+ return this.getCurrentValue(this.direction$);
31
+ }
32
+
33
+ /**
34
+ * @internal
35
+ */
36
+ setDirection(direction: CameraDirection) {
37
+ this.setCurrentValue(this.directionSubject, direction);
38
+ }
39
+
40
+ /**
41
+ * @internal
42
+ */
43
+ setMediaStream(stream: MediaStream | undefined): void {
44
+ super.setMediaStream(stream);
45
+ if (stream) {
46
+ // RN getSettings() doesn't return facingMode, so we don't verify camera direction
47
+ const direction = isReactNative()
48
+ ? this.direction
49
+ : stream.getVideoTracks()[0]?.getSettings().facingMode === 'environment'
50
+ ? 'back'
51
+ : 'front';
52
+ this.setDirection(direction);
53
+ }
54
+ }
55
+
56
+ protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
57
+ return stream.getVideoTracks()[0]?.getSettings().deviceId as
58
+ | string
59
+ | undefined;
60
+ }
61
+ }
@@ -0,0 +1,121 @@
1
+ import { Observable } from 'rxjs';
2
+ import { Call } from '../Call';
3
+ import { CallingState } from '../store';
4
+ import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
5
+ import { disposeOfMediaStream } from './devices';
6
+ import { isReactNative } from '../helpers/platforms';
7
+
8
+ export abstract class InputMediaDeviceManager<
9
+ T extends InputMediaDeviceManagerState,
10
+ > {
11
+ constructor(protected readonly call: Call, public readonly state: T) {}
12
+
13
+ /**
14
+ * Lists the available audio/video devices
15
+ *
16
+ * Note: It prompts the user for a permission to use devices (if not already granted)
17
+ *
18
+ * @returns an Observable that will be updated if a device is connected or disconnected
19
+ */
20
+ listDevices() {
21
+ return this.getDevices();
22
+ }
23
+
24
+ /**
25
+ * Starts camera/microphone
26
+ */
27
+ async enable() {
28
+ if (this.state.status === 'enabled') {
29
+ return;
30
+ }
31
+ await this.startStream();
32
+ this.state.setStatus('enabled');
33
+ }
34
+
35
+ /**
36
+ * Stops camera/microphone
37
+ * @returns
38
+ */
39
+ async disable() {
40
+ if (this.state.status === 'disabled') {
41
+ return;
42
+ }
43
+ await this.stopStream();
44
+ this.state.setStatus('disabled');
45
+ }
46
+
47
+ /**
48
+ * If current device statis is disabled, it will enable the device, else it will disable it.
49
+ * @returns
50
+ */
51
+ async toggle() {
52
+ if (this.state.status === 'enabled') {
53
+ return this.disable();
54
+ } else {
55
+ return this.enable();
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Select device
61
+ *
62
+ * Note: this method is not supported in React Native
63
+ *
64
+ * @param deviceId
65
+ */
66
+ async select(deviceId: string | undefined) {
67
+ if (isReactNative()) {
68
+ throw new Error('This method is not supported in React Native');
69
+ }
70
+ if (deviceId === this.state.selectedDevice) {
71
+ return;
72
+ }
73
+ this.state.setDevice(deviceId);
74
+ await this.applySettingsToStream();
75
+ }
76
+
77
+ protected async applySettingsToStream() {
78
+ if (this.state.status === 'enabled') {
79
+ await this.stopStream();
80
+ await this.startStream();
81
+ }
82
+ }
83
+
84
+ abstract pause(): void;
85
+
86
+ abstract resume(): void;
87
+
88
+ protected abstract getDevices(): Observable<MediaDeviceInfo[]>;
89
+
90
+ protected abstract getStream(
91
+ constraints: MediaTrackConstraints,
92
+ ): Promise<MediaStream>;
93
+
94
+ protected abstract publishStream(stream: MediaStream): Promise<void>;
95
+
96
+ protected abstract stopPublishStream(): Promise<void>;
97
+
98
+ private async stopStream() {
99
+ if (!this.state.mediaStream) {
100
+ return;
101
+ }
102
+ if (this.call.state.callingState === CallingState.JOINED) {
103
+ await this.stopPublishStream();
104
+ } else if (this.state.mediaStream) {
105
+ disposeOfMediaStream(this.state.mediaStream);
106
+ }
107
+ this.state.setMediaStream(undefined);
108
+ }
109
+
110
+ private async startStream() {
111
+ if (this.state.mediaStream) {
112
+ return;
113
+ }
114
+ const constraints = { deviceId: this.state.selectedDevice };
115
+ const stream = await this.getStream(constraints);
116
+ if (this.call.state.callingState === CallingState.JOINED) {
117
+ await this.publishStream(stream);
118
+ }
119
+ this.state.setMediaStream(stream);
120
+ }
121
+ }