@stream-io/video-client 1.43.0 → 1.44.0

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 (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.browser.es.js +206 -59
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +205 -58
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +206 -59
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/StreamVideoClient.d.ts +2 -8
  9. package/dist/src/coordinator/connection/types.d.ts +5 -0
  10. package/dist/src/devices/CameraManager.d.ts +7 -2
  11. package/dist/src/devices/DeviceManager.d.ts +7 -15
  12. package/dist/src/devices/MicrophoneManager.d.ts +2 -1
  13. package/dist/src/devices/SpeakerManager.d.ts +6 -1
  14. package/dist/src/devices/devicePersistence.d.ts +27 -0
  15. package/dist/src/helpers/clientUtils.d.ts +1 -1
  16. package/dist/src/permissions/PermissionsContext.d.ts +1 -1
  17. package/dist/src/types.d.ts +38 -2
  18. package/package.json +1 -1
  19. package/src/Call.ts +5 -3
  20. package/src/StreamVideoClient.ts +1 -9
  21. package/src/coordinator/connection/types.ts +6 -0
  22. package/src/devices/CameraManager.ts +31 -11
  23. package/src/devices/DeviceManager.ts +113 -31
  24. package/src/devices/MicrophoneManager.ts +26 -8
  25. package/src/devices/ScreenShareManager.ts +7 -1
  26. package/src/devices/SpeakerManager.ts +62 -18
  27. package/src/devices/__tests__/CameraManager.test.ts +184 -21
  28. package/src/devices/__tests__/DeviceManager.test.ts +184 -2
  29. package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -0
  30. package/src/devices/__tests__/MicrophoneManager.test.ts +146 -2
  31. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -0
  32. package/src/devices/__tests__/ScreenShareManager.test.ts +2 -0
  33. package/src/devices/__tests__/SpeakerManager.test.ts +90 -0
  34. package/src/devices/__tests__/devicePersistence.test.ts +142 -0
  35. package/src/devices/__tests__/devices.test.ts +390 -0
  36. package/src/devices/__tests__/mediaStreamTestHelpers.ts +58 -0
  37. package/src/devices/__tests__/mocks.ts +35 -0
  38. package/src/devices/devicePersistence.ts +106 -0
  39. package/src/devices/devices.ts +3 -3
  40. package/src/helpers/__tests__/DynascaleManager.test.ts +3 -1
  41. package/src/helpers/clientUtils.ts +1 -1
  42. package/src/permissions/PermissionsContext.ts +1 -0
  43. package/src/sorting/presets.ts +1 -1
  44. package/src/store/CallState.ts +1 -1
  45. package/src/types.ts +49 -2
package/dist/index.cjs.js CHANGED
@@ -5084,7 +5084,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
5084
5084
  /**
5085
5085
  * The sorting preset for livestreams and audio rooms.
5086
5086
  */
5087
- const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
5087
+ const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
5088
5088
 
5089
5089
  const ensureExhausted = (x, message) => {
5090
5090
  videoLoggerSystem.getLogger('helpers').warn(message, x);
@@ -5334,7 +5334,7 @@ class CallState {
5334
5334
  this.updateParticipant = (sessionId, patch) => {
5335
5335
  const participant = this.findParticipantBySessionId(sessionId);
5336
5336
  if (!participant) {
5337
- this.logger.warn(`Participant with sessionId ${sessionId} not found`);
5337
+ this.logger.debug(`Participant with sessionId ${sessionId} not found`);
5338
5338
  return;
5339
5339
  }
5340
5340
  const thePatch = typeof patch === 'function' ? patch(participant) : patch;
@@ -6251,7 +6251,7 @@ const getSdkVersion = (sdk) => {
6251
6251
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6252
6252
  };
6253
6253
 
6254
- const version = "1.43.0";
6254
+ const version = "1.44.0";
6255
6255
  const [major, minor, patch] = version.split('.');
6256
6256
  let sdkInfo = {
6257
6257
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -10019,6 +10019,7 @@ class PermissionsContext {
10019
10019
  return false;
10020
10020
  default:
10021
10021
  ensureExhausted(trackType, 'Unknown track type');
10022
+ return false;
10022
10023
  }
10023
10024
  };
10024
10025
  /**
@@ -10355,7 +10356,7 @@ const getDeviceChangeObserver = lazy((tracer) => {
10355
10356
  * the observable errors.
10356
10357
  */
10357
10358
  const getAudioDevices = lazy((tracer) => {
10358
- return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith(undefined), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), rxjs.shareReplay(1));
10359
+ return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), rxjs.shareReplay(1));
10359
10360
  });
10360
10361
  /**
10361
10362
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10364,7 +10365,7 @@ const getAudioDevices = lazy((tracer) => {
10364
10365
  * the observable errors.
10365
10366
  */
10366
10367
  const getVideoDevices = lazy((tracer) => {
10367
- return rxjs.merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(rxjs.startWith(undefined), rxjs.concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), rxjs.shareReplay(1));
10368
+ return rxjs.merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), rxjs.shareReplay(1));
10368
10369
  });
10369
10370
  /**
10370
10371
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10373,7 +10374,7 @@ const getVideoDevices = lazy((tracer) => {
10373
10374
  * the observable errors.
10374
10375
  */
10375
10376
  const getAudioOutputDevices = lazy((tracer) => {
10376
- return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith(undefined), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), rxjs.shareReplay(1));
10377
+ return rxjs.merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith([]), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), rxjs.shareReplay(1));
10377
10378
  });
10378
10379
  let getUserMediaExecId = 0;
10379
10380
  const getStream = async (constraints, tracer) => {
@@ -10580,25 +10581,66 @@ function resolveDeviceId(deviceId, kind) {
10580
10581
  */
10581
10582
  const isMobile = () => /Mobi/i.test(navigator.userAgent);
10582
10583
 
10584
+ const defaultDeviceId = 'default';
10585
+ const isLocalStorageAvailable = () => typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
10586
+ const normalize = (options) => {
10587
+ return {
10588
+ storageKey: options?.storageKey ?? `@stream-io/device-preferences`,
10589
+ enabled: isLocalStorageAvailable() && !isReactNative()
10590
+ ? (options?.enabled ?? true)
10591
+ : false,
10592
+ };
10593
+ };
10594
+ const createSyntheticDevice = (deviceId, kind) => {
10595
+ return { deviceId, kind, label: '', groupId: '' };
10596
+ };
10597
+ const readPreferences = (storageKey) => {
10598
+ try {
10599
+ const raw = window.localStorage.getItem(storageKey) || '{}';
10600
+ return JSON.parse(raw);
10601
+ }
10602
+ catch {
10603
+ return {};
10604
+ }
10605
+ };
10606
+ const writePreferences = (currentDevice, deviceKey, muted, storageKey) => {
10607
+ if (!isLocalStorageAvailable())
10608
+ return;
10609
+ const selectedDeviceId = currentDevice?.deviceId ?? defaultDeviceId;
10610
+ const selectedDeviceLabel = currentDevice?.label ?? '';
10611
+ const preferences = readPreferences(storageKey);
10612
+ const preferenceHistory = [preferences[deviceKey] ?? []]
10613
+ .flat()
10614
+ .filter((p) => p.selectedDeviceId !== selectedDeviceId &&
10615
+ (p.selectedDeviceLabel === '' ||
10616
+ p.selectedDeviceLabel !== selectedDeviceLabel));
10617
+ const nextPreferences = {
10618
+ ...preferences,
10619
+ [deviceKey]: [
10620
+ {
10621
+ selectedDeviceId,
10622
+ selectedDeviceLabel,
10623
+ ...(typeof muted === 'boolean' ? { muted } : {}),
10624
+ },
10625
+ ...preferenceHistory,
10626
+ ].slice(0, 3),
10627
+ };
10628
+ try {
10629
+ window.localStorage.setItem(storageKey, JSON.stringify(nextPreferences));
10630
+ }
10631
+ catch (err) {
10632
+ const logger = videoLoggerSystem.getLogger('DevicePersistence');
10633
+ logger.error('failed to save device preferences', err);
10634
+ }
10635
+ };
10636
+ const toPreferenceList = (preference) => (preference ? [preference].flat() : []);
10637
+
10583
10638
  class DeviceManager {
10584
- constructor(call, state, trackType) {
10639
+ constructor(call, state, trackType, devicePersistence) {
10585
10640
  /**
10586
10641
  * if true, stops the media stream when call is left
10587
10642
  */
10588
10643
  this.stopOnLeave = true;
10589
- /**
10590
- * When `true`, the `apply()` method will skip automatically enabling/disabling
10591
- * the device based on server defaults (`mic_default_on`, `camera_default_on`).
10592
- *
10593
- * This is useful when application code wants to handle device preferences
10594
- * (e.g., persisted user preferences) and prevent server defaults from
10595
- * overriding them.
10596
- *
10597
- * @default false
10598
- *
10599
- * @internal
10600
- */
10601
- this.deferServerDefaults = false;
10602
10644
  this.subscriptions = [];
10603
10645
  this.areSubscriptionsSetUp = false;
10604
10646
  this.isTrackStoppedDueToTrackEnd = false;
@@ -10618,19 +10660,26 @@ class DeviceManager {
10618
10660
  this.call = call;
10619
10661
  this.state = state;
10620
10662
  this.trackType = trackType;
10663
+ this.devicePersistence = devicePersistence;
10621
10664
  this.logger = videoLoggerSystem.getLogger(`${TrackType[trackType].toLowerCase()} manager`);
10622
10665
  this.setup();
10623
10666
  }
10624
10667
  setup() {
10625
- if (this.areSubscriptionsSetUp) {
10668
+ if (this.areSubscriptionsSetUp)
10626
10669
  return;
10627
- }
10628
10670
  this.areSubscriptionsSetUp = true;
10629
10671
  if (deviceIds$ &&
10630
10672
  !isReactNative() &&
10631
10673
  (this.trackType === TrackType.AUDIO || this.trackType === TrackType.VIDEO)) {
10632
10674
  this.handleDisconnectedOrReplacedDevices();
10633
10675
  }
10676
+ if (this.devicePersistence.enabled) {
10677
+ this.subscriptions.push(createSubscription(rxjs.combineLatest([this.state.selectedDevice$, this.state.status$]), ([selectedDevice, status]) => {
10678
+ if (!status)
10679
+ return;
10680
+ this.persistPreference(selectedDevice, status);
10681
+ }));
10682
+ }
10634
10683
  }
10635
10684
  /**
10636
10685
  * Lists the available audio/video devices
@@ -10981,13 +11030,11 @@ class DeviceManager {
10981
11030
  }
10982
11031
  }
10983
11032
  get mediaDeviceKind() {
10984
- if (this.trackType === TrackType.AUDIO) {
11033
+ if (this.trackType === TrackType.AUDIO)
10985
11034
  return 'audioinput';
10986
- }
10987
- if (this.trackType === TrackType.VIDEO) {
11035
+ if (this.trackType === TrackType.VIDEO)
10988
11036
  return 'videoinput';
10989
- }
10990
- return '';
11037
+ throw new Error('Invalid track type');
10991
11038
  }
10992
11039
  handleDisconnectedOrReplacedDevices() {
10993
11040
  this.subscriptions.push(createSubscription(rxjs.combineLatest([
@@ -11035,6 +11082,62 @@ class DeviceManager {
11035
11082
  const kind = this.mediaDeviceKind;
11036
11083
  return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
11037
11084
  }
11085
+ persistPreference(selectedDevice, status) {
11086
+ const deviceKind = this.mediaDeviceKind;
11087
+ const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
11088
+ const muted = status === 'disabled' ? true : status === 'enabled' ? false : undefined;
11089
+ const { storageKey } = this.devicePersistence;
11090
+ if (!selectedDevice) {
11091
+ writePreferences(undefined, deviceKey, muted, storageKey);
11092
+ return;
11093
+ }
11094
+ const devices = getCurrentValue(this.listDevices()) || [];
11095
+ const currentDevice = this.findDevice(devices, selectedDevice) ??
11096
+ createSyntheticDevice(selectedDevice, deviceKind);
11097
+ writePreferences(currentDevice, deviceKey, muted, storageKey);
11098
+ }
11099
+ async applyPersistedPreferences(enabledInCallType) {
11100
+ const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
11101
+ const preferences = readPreferences(this.devicePersistence.storageKey);
11102
+ const preferenceList = toPreferenceList(preferences[deviceKey]);
11103
+ if (preferenceList.length === 0)
11104
+ return false;
11105
+ let muted;
11106
+ let appliedDevice = false;
11107
+ let appliedMute = false;
11108
+ const devices = await rxjs.firstValueFrom(this.listDevices());
11109
+ for (const preference of preferenceList) {
11110
+ muted ?? (muted = preference.muted);
11111
+ if (preference.selectedDeviceId === defaultDeviceId)
11112
+ break;
11113
+ const device = devices.find((d) => d.deviceId === preference.selectedDeviceId) ??
11114
+ devices.find((d) => d.label === preference.selectedDeviceLabel);
11115
+ if (device) {
11116
+ appliedDevice = true;
11117
+ if (!this.state.selectedDevice) {
11118
+ await this.select(device.deviceId);
11119
+ }
11120
+ muted = preference.muted;
11121
+ break;
11122
+ }
11123
+ }
11124
+ const canPublish = this.call.permissionsContext.canPublish(this.trackType);
11125
+ if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
11126
+ await this.applyMutedState(muted);
11127
+ appliedMute = true;
11128
+ }
11129
+ return appliedDevice || appliedMute;
11130
+ }
11131
+ async applyMutedState(muted) {
11132
+ if (this.state.status !== undefined)
11133
+ return;
11134
+ if (muted) {
11135
+ await this.disable();
11136
+ }
11137
+ else {
11138
+ await this.enable();
11139
+ }
11140
+ }
11038
11141
  }
11039
11142
 
11040
11143
  class DeviceManagerState {
@@ -11216,9 +11319,10 @@ class CameraManager extends DeviceManager {
11216
11319
  * Constructs a new CameraManager.
11217
11320
  *
11218
11321
  * @param call the call instance.
11322
+ * @param devicePersistence the device persistence preferences to use.
11219
11323
  */
11220
- constructor(call) {
11221
- super(call, new CameraManagerState(), TrackType.VIDEO);
11324
+ constructor(call, devicePersistence) {
11325
+ super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
11222
11326
  this.targetResolution = {
11223
11327
  width: 1280,
11224
11328
  height: 720,
@@ -11231,8 +11335,9 @@ class CameraManager extends DeviceManager {
11231
11335
  * Select the camera direction.
11232
11336
  *
11233
11337
  * @param direction the direction of the camera to select.
11338
+ * @param options additional direction selection options.
11234
11339
  */
11235
- async selectDirection(direction) {
11340
+ async selectDirection(direction, options = {}) {
11236
11341
  if (!this.isDirectionSupportedByDevice()) {
11237
11342
  this.logger.warn('Setting direction is not supported on this device');
11238
11343
  return;
@@ -11246,9 +11351,9 @@ class CameraManager extends DeviceManager {
11246
11351
  // providing both device id and direction doesn't work, so we deselect the device
11247
11352
  this.state.setDirection(direction);
11248
11353
  this.state.setDevice(undefined);
11249
- if (isReactNative()) {
11354
+ const { enableCamera = true } = options;
11355
+ if (isReactNative() || !enableCamera)
11250
11356
  return;
11251
- }
11252
11357
  this.getTracks().forEach((track) => track.stop());
11253
11358
  try {
11254
11359
  await this.unmuteStream();
@@ -11316,15 +11421,23 @@ class CameraManager extends DeviceManager {
11316
11421
  // Wait for any in progress camera operation
11317
11422
  await this.statusChangeSettled();
11318
11423
  await this.selectTargetResolution(settings.target_resolution);
11319
- // apply a direction and enable the camera only if in "pristine" state
11320
- // and server defaults are not deferred to application code
11424
+ const enabledInCallType = settings.enabled ?? true;
11425
+ const shouldApplyDefaults = this.state.status === undefined &&
11426
+ this.state.optimisticStatus === undefined;
11427
+ let persistedPreferencesApplied = false;
11428
+ if (shouldApplyDefaults && this.devicePersistence.enabled) {
11429
+ persistedPreferencesApplied =
11430
+ await this.applyPersistedPreferences(enabledInCallType);
11431
+ }
11432
+ // apply a direction and enable the camera only if in "pristine" state,
11433
+ // and there are no persisted preferences
11321
11434
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
11322
- if (this.state.status === undefined && !this.deferServerDefaults) {
11435
+ if (shouldApplyDefaults && !persistedPreferencesApplied) {
11323
11436
  if (!this.state.direction && !this.state.selectedDevice) {
11324
11437
  const direction = settings.camera_facing === 'front' ? 'front' : 'back';
11325
- await this.selectDirection(direction);
11438
+ await this.selectDirection(direction, { enableCamera: false });
11326
11439
  }
11327
- if (canPublish && settings.camera_default_on && settings.enabled) {
11440
+ if (canPublish && settings.camera_default_on && enabledInCallType) {
11328
11441
  await this.enable();
11329
11442
  }
11330
11443
  }
@@ -11760,8 +11873,8 @@ class RNSpeechDetector {
11760
11873
  }
11761
11874
 
11762
11875
  class MicrophoneManager extends AudioDeviceManager {
11763
- constructor(call, disableMode = 'stop-tracks') {
11764
- super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
11876
+ constructor(call, devicePersistence, disableMode = 'stop-tracks') {
11877
+ super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO, devicePersistence);
11765
11878
  this.speakingWhileMutedNotificationEnabled = true;
11766
11879
  this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11767
11880
  this.silenceThresholdMs = 5000;
@@ -11850,7 +11963,7 @@ class MicrophoneManager extends AudioDeviceManager {
11850
11963
  if (this.silenceThresholdMs <= 0)
11851
11964
  return;
11852
11965
  const deviceId = this.state.selectedDevice;
11853
- const devices = getCurrentValue(this.listDevices());
11966
+ const devices = await rxjs.firstValueFrom(this.listDevices());
11854
11967
  const label = devices.find((d) => d.deviceId === deviceId)?.label;
11855
11968
  this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
11856
11969
  noAudioThresholdMs: this.silenceThresholdMs,
@@ -11863,6 +11976,7 @@ class MicrophoneManager extends AudioDeviceManager {
11863
11976
  deviceId,
11864
11977
  label,
11865
11978
  };
11979
+ console.log(event);
11866
11980
  this.call.tracer.trace('mic.capture_report', event);
11867
11981
  this.call.streamClient.dispatchEvent(event);
11868
11982
  },
@@ -12014,10 +12128,16 @@ class MicrophoneManager extends AudioDeviceManager {
12014
12128
  async apply(settings, publish) {
12015
12129
  // Wait for any in progress mic operation
12016
12130
  await this.statusChangeSettled();
12017
- const canPublish = this.call.permissionsContext.canPublish(this.trackType);
12018
12131
  // apply server-side settings only when the device state is pristine
12019
- // and server defaults are not deferred to application code
12020
- if (this.state.status === undefined && !this.deferServerDefaults) {
12132
+ // and there are no persisted preferences
12133
+ const shouldApplyDefaults = this.state.status === undefined &&
12134
+ this.state.optimisticStatus === undefined;
12135
+ let persistedPreferencesApplied = false;
12136
+ if (shouldApplyDefaults && this.devicePersistence.enabled) {
12137
+ persistedPreferencesApplied = await this.applyPersistedPreferences(true);
12138
+ }
12139
+ const canPublish = this.call.permissionsContext.canPublish(this.trackType);
12140
+ if (shouldApplyDefaults && !persistedPreferencesApplied) {
12021
12141
  if (canPublish && settings.mic_default_on) {
12022
12142
  await this.enable();
12023
12143
  }
@@ -12168,7 +12288,7 @@ class ScreenShareState extends AudioDeviceManagerState {
12168
12288
 
12169
12289
  class ScreenShareManager extends AudioDeviceManager {
12170
12290
  constructor(call) {
12171
- super(call, new ScreenShareState(), TrackType.SCREEN_SHARE);
12291
+ super(call, new ScreenShareState(), TrackType.SCREEN_SHARE, normalize({ enabled: false }));
12172
12292
  }
12173
12293
  setup() {
12174
12294
  if (this.areSubscriptionsSetUp)
@@ -12317,7 +12437,7 @@ class SpeakerState {
12317
12437
  }
12318
12438
 
12319
12439
  class SpeakerManager {
12320
- constructor(call) {
12440
+ constructor(call, devicePreferences) {
12321
12441
  this.subscriptions = [];
12322
12442
  this.areSubscriptionsSetUp = false;
12323
12443
  /**
@@ -12326,18 +12446,35 @@ class SpeakerManager {
12326
12446
  * @internal
12327
12447
  */
12328
12448
  this.dispose = () => {
12329
- this.subscriptions.forEach((s) => s.unsubscribe());
12449
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
12330
12450
  this.subscriptions = [];
12331
12451
  this.areSubscriptionsSetUp = false;
12332
12452
  };
12333
12453
  this.call = call;
12334
12454
  this.state = new SpeakerState(call.tracer);
12455
+ this.devicePersistence = devicePreferences;
12335
12456
  this.setup();
12336
12457
  }
12337
12458
  apply(settings) {
12338
- if (!isReactNative()) {
12459
+ return isReactNative() ? this.applyRN(settings) : this.applyWeb();
12460
+ }
12461
+ applyWeb() {
12462
+ const { enabled, storageKey } = this.devicePersistence;
12463
+ if (!enabled)
12339
12464
  return;
12465
+ const preferences = readPreferences(storageKey);
12466
+ const preferenceList = toPreferenceList(preferences.speaker);
12467
+ if (preferenceList.length === 0)
12468
+ return;
12469
+ const preference = preferenceList[0];
12470
+ const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
12471
+ ? ''
12472
+ : preference.selectedDeviceId;
12473
+ if (this.state.selectedDevice !== nextDeviceId) {
12474
+ this.select(nextDeviceId);
12340
12475
  }
12476
+ }
12477
+ applyRN(settings) {
12341
12478
  /// Determines if the speaker should be enabled based on a priority hierarchy of
12342
12479
  /// settings.
12343
12480
  ///
@@ -12366,19 +12503,21 @@ class SpeakerManager {
12366
12503
  }
12367
12504
  }
12368
12505
  setup() {
12369
- if (this.areSubscriptionsSetUp) {
12506
+ if (this.areSubscriptionsSetUp)
12370
12507
  return;
12371
- }
12372
12508
  this.areSubscriptionsSetUp = true;
12373
12509
  if (deviceIds$ && !isReactNative()) {
12374
- this.subscriptions.push(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
12375
- if (!deviceId) {
12510
+ this.subscriptions.push(createSubscription(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]), ([devices, deviceId]) => {
12511
+ if (!deviceId)
12376
12512
  return;
12377
- }
12378
12513
  const device = devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
12379
- if (!device) {
12514
+ if (!device)
12380
12515
  this.select('');
12381
- }
12516
+ }));
12517
+ }
12518
+ if (!isReactNative() && this.devicePersistence.enabled) {
12519
+ this.subscriptions.push(createSubscription(this.state.selectedDevice$, (selectedDevice) => {
12520
+ this.persistSpeakerDevicePreference(selectedDevice);
12382
12521
  }));
12383
12522
  }
12384
12523
  }
@@ -12438,6 +12577,13 @@ class SpeakerManager {
12438
12577
  return { audioVolume: volume };
12439
12578
  });
12440
12579
  }
12580
+ persistSpeakerDevicePreference(selectedDevice) {
12581
+ const { storageKey } = this.devicePersistence;
12582
+ const devices = getCurrentValue(this.listDevices()) || [];
12583
+ const currentDevice = devices.find((d) => d.deviceId === selectedDevice) ??
12584
+ createSyntheticDevice(selectedDevice, 'audiooutput');
12585
+ writePreferences(currentDevice, 'speaker', undefined, storageKey);
12586
+ }
12441
12587
  }
12442
12588
  const assertUnsupportedInReactNative = () => {
12443
12589
  if (isReactNative()) {
@@ -14542,9 +14688,10 @@ class Call {
14542
14688
  this.state.setMembers(members || []);
14543
14689
  this.state.setOwnCapabilities(ownCapabilities || []);
14544
14690
  this.state.setCallingState(ringing ? exports.CallingState.RINGING : exports.CallingState.IDLE);
14545
- this.camera = new CameraManager(this);
14546
- this.microphone = new MicrophoneManager(this);
14547
- this.speaker = new SpeakerManager(this);
14691
+ const preferences = normalize(streamClient.options.devicePersistence);
14692
+ this.camera = new CameraManager(this, preferences);
14693
+ this.microphone = new MicrophoneManager(this, preferences);
14694
+ this.speaker = new SpeakerManager(this, preferences);
14548
14695
  this.screenShare = new ScreenShareManager(this);
14549
14696
  this.dynascaleManager = new DynascaleManager(this.state, this.speaker, this.tracer);
14550
14697
  }
@@ -15688,7 +15835,7 @@ class StreamClient {
15688
15835
  this.getUserAgent = () => {
15689
15836
  if (!this.cachedUserAgent) {
15690
15837
  const { clientAppIdentifier = {} } = this.options;
15691
- const { sdkName = 'js', sdkVersion = "1.43.0", ...extras } = clientAppIdentifier;
15838
+ const { sdkName = 'js', sdkVersion = "1.44.0", ...extras } = clientAppIdentifier;
15692
15839
  this.cachedUserAgent = [
15693
15840
  `stream-video-${sdkName}-v${sdkVersion}`,
15694
15841
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),