@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.es.js CHANGED
@@ -7,7 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
7
7
  import * as scopedLogger from '@stream-io/logger';
8
8
  export { LogLevelEnum } from '@stream-io/logger';
9
9
  import { parse, write } from 'sdp-transform';
10
- import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, of } from 'rxjs';
10
+ import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of } from 'rxjs';
11
11
  import { UAParser } from 'ua-parser-js';
12
12
  import { WorkerTimer } from '@stream-io/worker-timer';
13
13
  import https from 'https';
@@ -5065,7 +5065,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
5065
5065
  /**
5066
5066
  * The sorting preset for livestreams and audio rooms.
5067
5067
  */
5068
- const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
5068
+ const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
5069
5069
 
5070
5070
  const ensureExhausted = (x, message) => {
5071
5071
  videoLoggerSystem.getLogger('helpers').warn(message, x);
@@ -5315,7 +5315,7 @@ class CallState {
5315
5315
  this.updateParticipant = (sessionId, patch) => {
5316
5316
  const participant = this.findParticipantBySessionId(sessionId);
5317
5317
  if (!participant) {
5318
- this.logger.warn(`Participant with sessionId ${sessionId} not found`);
5318
+ this.logger.debug(`Participant with sessionId ${sessionId} not found`);
5319
5319
  return;
5320
5320
  }
5321
5321
  const thePatch = typeof patch === 'function' ? patch(participant) : patch;
@@ -6232,7 +6232,7 @@ const getSdkVersion = (sdk) => {
6232
6232
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6233
6233
  };
6234
6234
 
6235
- const version = "1.43.0";
6235
+ const version = "1.44.0";
6236
6236
  const [major, minor, patch] = version.split('.');
6237
6237
  let sdkInfo = {
6238
6238
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -10000,6 +10000,7 @@ class PermissionsContext {
10000
10000
  return false;
10001
10001
  default:
10002
10002
  ensureExhausted(trackType, 'Unknown track type');
10003
+ return false;
10003
10004
  }
10004
10005
  };
10005
10006
  /**
@@ -10336,7 +10337,7 @@ const getDeviceChangeObserver = lazy((tracer) => {
10336
10337
  * the observable errors.
10337
10338
  */
10338
10339
  const getAudioDevices = lazy((tracer) => {
10339
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith(undefined), concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), shareReplay(1));
10340
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput', tracer)), shareReplay(1));
10340
10341
  });
10341
10342
  /**
10342
10343
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10345,7 +10346,7 @@ const getAudioDevices = lazy((tracer) => {
10345
10346
  * the observable errors.
10346
10347
  */
10347
10348
  const getVideoDevices = lazy((tracer) => {
10348
- return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(startWith(undefined), concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), shareReplay(1));
10349
+ return merge(getDeviceChangeObserver(tracer), getVideoBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput', tracer)), shareReplay(1));
10349
10350
  });
10350
10351
  /**
10351
10352
  * Prompts the user for a permission to use video devices (if not already granted
@@ -10354,7 +10355,7 @@ const getVideoDevices = lazy((tracer) => {
10354
10355
  * the observable errors.
10355
10356
  */
10356
10357
  const getAudioOutputDevices = lazy((tracer) => {
10357
- return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith(undefined), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), shareReplay(1));
10358
+ return merge(getDeviceChangeObserver(tracer), getAudioBrowserPermission().asObservable()).pipe(startWith([]), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput', tracer)), shareReplay(1));
10358
10359
  });
10359
10360
  let getUserMediaExecId = 0;
10360
10361
  const getStream = async (constraints, tracer) => {
@@ -10561,25 +10562,66 @@ function resolveDeviceId(deviceId, kind) {
10561
10562
  */
10562
10563
  const isMobile = () => /Mobi/i.test(navigator.userAgent);
10563
10564
 
10565
+ const defaultDeviceId = 'default';
10566
+ const isLocalStorageAvailable = () => typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
10567
+ const normalize = (options) => {
10568
+ return {
10569
+ storageKey: options?.storageKey ?? `@stream-io/device-preferences`,
10570
+ enabled: isLocalStorageAvailable() && !isReactNative()
10571
+ ? (options?.enabled ?? true)
10572
+ : false,
10573
+ };
10574
+ };
10575
+ const createSyntheticDevice = (deviceId, kind) => {
10576
+ return { deviceId, kind, label: '', groupId: '' };
10577
+ };
10578
+ const readPreferences = (storageKey) => {
10579
+ try {
10580
+ const raw = window.localStorage.getItem(storageKey) || '{}';
10581
+ return JSON.parse(raw);
10582
+ }
10583
+ catch {
10584
+ return {};
10585
+ }
10586
+ };
10587
+ const writePreferences = (currentDevice, deviceKey, muted, storageKey) => {
10588
+ if (!isLocalStorageAvailable())
10589
+ return;
10590
+ const selectedDeviceId = currentDevice?.deviceId ?? defaultDeviceId;
10591
+ const selectedDeviceLabel = currentDevice?.label ?? '';
10592
+ const preferences = readPreferences(storageKey);
10593
+ const preferenceHistory = [preferences[deviceKey] ?? []]
10594
+ .flat()
10595
+ .filter((p) => p.selectedDeviceId !== selectedDeviceId &&
10596
+ (p.selectedDeviceLabel === '' ||
10597
+ p.selectedDeviceLabel !== selectedDeviceLabel));
10598
+ const nextPreferences = {
10599
+ ...preferences,
10600
+ [deviceKey]: [
10601
+ {
10602
+ selectedDeviceId,
10603
+ selectedDeviceLabel,
10604
+ ...(typeof muted === 'boolean' ? { muted } : {}),
10605
+ },
10606
+ ...preferenceHistory,
10607
+ ].slice(0, 3),
10608
+ };
10609
+ try {
10610
+ window.localStorage.setItem(storageKey, JSON.stringify(nextPreferences));
10611
+ }
10612
+ catch (err) {
10613
+ const logger = videoLoggerSystem.getLogger('DevicePersistence');
10614
+ logger.error('failed to save device preferences', err);
10615
+ }
10616
+ };
10617
+ const toPreferenceList = (preference) => (preference ? [preference].flat() : []);
10618
+
10564
10619
  class DeviceManager {
10565
- constructor(call, state, trackType) {
10620
+ constructor(call, state, trackType, devicePersistence) {
10566
10621
  /**
10567
10622
  * if true, stops the media stream when call is left
10568
10623
  */
10569
10624
  this.stopOnLeave = true;
10570
- /**
10571
- * When `true`, the `apply()` method will skip automatically enabling/disabling
10572
- * the device based on server defaults (`mic_default_on`, `camera_default_on`).
10573
- *
10574
- * This is useful when application code wants to handle device preferences
10575
- * (e.g., persisted user preferences) and prevent server defaults from
10576
- * overriding them.
10577
- *
10578
- * @default false
10579
- *
10580
- * @internal
10581
- */
10582
- this.deferServerDefaults = false;
10583
10625
  this.subscriptions = [];
10584
10626
  this.areSubscriptionsSetUp = false;
10585
10627
  this.isTrackStoppedDueToTrackEnd = false;
@@ -10599,19 +10641,26 @@ class DeviceManager {
10599
10641
  this.call = call;
10600
10642
  this.state = state;
10601
10643
  this.trackType = trackType;
10644
+ this.devicePersistence = devicePersistence;
10602
10645
  this.logger = videoLoggerSystem.getLogger(`${TrackType[trackType].toLowerCase()} manager`);
10603
10646
  this.setup();
10604
10647
  }
10605
10648
  setup() {
10606
- if (this.areSubscriptionsSetUp) {
10649
+ if (this.areSubscriptionsSetUp)
10607
10650
  return;
10608
- }
10609
10651
  this.areSubscriptionsSetUp = true;
10610
10652
  if (deviceIds$ &&
10611
10653
  !isReactNative() &&
10612
10654
  (this.trackType === TrackType.AUDIO || this.trackType === TrackType.VIDEO)) {
10613
10655
  this.handleDisconnectedOrReplacedDevices();
10614
10656
  }
10657
+ if (this.devicePersistence.enabled) {
10658
+ this.subscriptions.push(createSubscription(combineLatest([this.state.selectedDevice$, this.state.status$]), ([selectedDevice, status]) => {
10659
+ if (!status)
10660
+ return;
10661
+ this.persistPreference(selectedDevice, status);
10662
+ }));
10663
+ }
10615
10664
  }
10616
10665
  /**
10617
10666
  * Lists the available audio/video devices
@@ -10962,13 +11011,11 @@ class DeviceManager {
10962
11011
  }
10963
11012
  }
10964
11013
  get mediaDeviceKind() {
10965
- if (this.trackType === TrackType.AUDIO) {
11014
+ if (this.trackType === TrackType.AUDIO)
10966
11015
  return 'audioinput';
10967
- }
10968
- if (this.trackType === TrackType.VIDEO) {
11016
+ if (this.trackType === TrackType.VIDEO)
10969
11017
  return 'videoinput';
10970
- }
10971
- return '';
11018
+ throw new Error('Invalid track type');
10972
11019
  }
10973
11020
  handleDisconnectedOrReplacedDevices() {
10974
11021
  this.subscriptions.push(createSubscription(combineLatest([
@@ -11016,6 +11063,62 @@ class DeviceManager {
11016
11063
  const kind = this.mediaDeviceKind;
11017
11064
  return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
11018
11065
  }
11066
+ persistPreference(selectedDevice, status) {
11067
+ const deviceKind = this.mediaDeviceKind;
11068
+ const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
11069
+ const muted = status === 'disabled' ? true : status === 'enabled' ? false : undefined;
11070
+ const { storageKey } = this.devicePersistence;
11071
+ if (!selectedDevice) {
11072
+ writePreferences(undefined, deviceKey, muted, storageKey);
11073
+ return;
11074
+ }
11075
+ const devices = getCurrentValue(this.listDevices()) || [];
11076
+ const currentDevice = this.findDevice(devices, selectedDevice) ??
11077
+ createSyntheticDevice(selectedDevice, deviceKind);
11078
+ writePreferences(currentDevice, deviceKey, muted, storageKey);
11079
+ }
11080
+ async applyPersistedPreferences(enabledInCallType) {
11081
+ const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
11082
+ const preferences = readPreferences(this.devicePersistence.storageKey);
11083
+ const preferenceList = toPreferenceList(preferences[deviceKey]);
11084
+ if (preferenceList.length === 0)
11085
+ return false;
11086
+ let muted;
11087
+ let appliedDevice = false;
11088
+ let appliedMute = false;
11089
+ const devices = await firstValueFrom(this.listDevices());
11090
+ for (const preference of preferenceList) {
11091
+ muted ?? (muted = preference.muted);
11092
+ if (preference.selectedDeviceId === defaultDeviceId)
11093
+ break;
11094
+ const device = devices.find((d) => d.deviceId === preference.selectedDeviceId) ??
11095
+ devices.find((d) => d.label === preference.selectedDeviceLabel);
11096
+ if (device) {
11097
+ appliedDevice = true;
11098
+ if (!this.state.selectedDevice) {
11099
+ await this.select(device.deviceId);
11100
+ }
11101
+ muted = preference.muted;
11102
+ break;
11103
+ }
11104
+ }
11105
+ const canPublish = this.call.permissionsContext.canPublish(this.trackType);
11106
+ if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
11107
+ await this.applyMutedState(muted);
11108
+ appliedMute = true;
11109
+ }
11110
+ return appliedDevice || appliedMute;
11111
+ }
11112
+ async applyMutedState(muted) {
11113
+ if (this.state.status !== undefined)
11114
+ return;
11115
+ if (muted) {
11116
+ await this.disable();
11117
+ }
11118
+ else {
11119
+ await this.enable();
11120
+ }
11121
+ }
11019
11122
  }
11020
11123
 
11021
11124
  class DeviceManagerState {
@@ -11197,9 +11300,10 @@ class CameraManager extends DeviceManager {
11197
11300
  * Constructs a new CameraManager.
11198
11301
  *
11199
11302
  * @param call the call instance.
11303
+ * @param devicePersistence the device persistence preferences to use.
11200
11304
  */
11201
- constructor(call) {
11202
- super(call, new CameraManagerState(), TrackType.VIDEO);
11305
+ constructor(call, devicePersistence) {
11306
+ super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
11203
11307
  this.targetResolution = {
11204
11308
  width: 1280,
11205
11309
  height: 720,
@@ -11212,8 +11316,9 @@ class CameraManager extends DeviceManager {
11212
11316
  * Select the camera direction.
11213
11317
  *
11214
11318
  * @param direction the direction of the camera to select.
11319
+ * @param options additional direction selection options.
11215
11320
  */
11216
- async selectDirection(direction) {
11321
+ async selectDirection(direction, options = {}) {
11217
11322
  if (!this.isDirectionSupportedByDevice()) {
11218
11323
  this.logger.warn('Setting direction is not supported on this device');
11219
11324
  return;
@@ -11227,9 +11332,9 @@ class CameraManager extends DeviceManager {
11227
11332
  // providing both device id and direction doesn't work, so we deselect the device
11228
11333
  this.state.setDirection(direction);
11229
11334
  this.state.setDevice(undefined);
11230
- if (isReactNative()) {
11335
+ const { enableCamera = true } = options;
11336
+ if (isReactNative() || !enableCamera)
11231
11337
  return;
11232
- }
11233
11338
  this.getTracks().forEach((track) => track.stop());
11234
11339
  try {
11235
11340
  await this.unmuteStream();
@@ -11297,15 +11402,23 @@ class CameraManager extends DeviceManager {
11297
11402
  // Wait for any in progress camera operation
11298
11403
  await this.statusChangeSettled();
11299
11404
  await this.selectTargetResolution(settings.target_resolution);
11300
- // apply a direction and enable the camera only if in "pristine" state
11301
- // and server defaults are not deferred to application code
11405
+ const enabledInCallType = settings.enabled ?? true;
11406
+ const shouldApplyDefaults = this.state.status === undefined &&
11407
+ this.state.optimisticStatus === undefined;
11408
+ let persistedPreferencesApplied = false;
11409
+ if (shouldApplyDefaults && this.devicePersistence.enabled) {
11410
+ persistedPreferencesApplied =
11411
+ await this.applyPersistedPreferences(enabledInCallType);
11412
+ }
11413
+ // apply a direction and enable the camera only if in "pristine" state,
11414
+ // and there are no persisted preferences
11302
11415
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
11303
- if (this.state.status === undefined && !this.deferServerDefaults) {
11416
+ if (shouldApplyDefaults && !persistedPreferencesApplied) {
11304
11417
  if (!this.state.direction && !this.state.selectedDevice) {
11305
11418
  const direction = settings.camera_facing === 'front' ? 'front' : 'back';
11306
- await this.selectDirection(direction);
11419
+ await this.selectDirection(direction, { enableCamera: false });
11307
11420
  }
11308
- if (canPublish && settings.camera_default_on && settings.enabled) {
11421
+ if (canPublish && settings.camera_default_on && enabledInCallType) {
11309
11422
  await this.enable();
11310
11423
  }
11311
11424
  }
@@ -11741,8 +11854,8 @@ class RNSpeechDetector {
11741
11854
  }
11742
11855
 
11743
11856
  class MicrophoneManager extends AudioDeviceManager {
11744
- constructor(call, disableMode = 'stop-tracks') {
11745
- super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
11857
+ constructor(call, devicePersistence, disableMode = 'stop-tracks') {
11858
+ super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO, devicePersistence);
11746
11859
  this.speakingWhileMutedNotificationEnabled = true;
11747
11860
  this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
11748
11861
  this.silenceThresholdMs = 5000;
@@ -11831,7 +11944,7 @@ class MicrophoneManager extends AudioDeviceManager {
11831
11944
  if (this.silenceThresholdMs <= 0)
11832
11945
  return;
11833
11946
  const deviceId = this.state.selectedDevice;
11834
- const devices = getCurrentValue(this.listDevices());
11947
+ const devices = await firstValueFrom(this.listDevices());
11835
11948
  const label = devices.find((d) => d.deviceId === deviceId)?.label;
11836
11949
  this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
11837
11950
  noAudioThresholdMs: this.silenceThresholdMs,
@@ -11844,6 +11957,7 @@ class MicrophoneManager extends AudioDeviceManager {
11844
11957
  deviceId,
11845
11958
  label,
11846
11959
  };
11960
+ console.log(event);
11847
11961
  this.call.tracer.trace('mic.capture_report', event);
11848
11962
  this.call.streamClient.dispatchEvent(event);
11849
11963
  },
@@ -11995,10 +12109,16 @@ class MicrophoneManager extends AudioDeviceManager {
11995
12109
  async apply(settings, publish) {
11996
12110
  // Wait for any in progress mic operation
11997
12111
  await this.statusChangeSettled();
11998
- const canPublish = this.call.permissionsContext.canPublish(this.trackType);
11999
12112
  // apply server-side settings only when the device state is pristine
12000
- // and server defaults are not deferred to application code
12001
- if (this.state.status === undefined && !this.deferServerDefaults) {
12113
+ // and there are no persisted preferences
12114
+ const shouldApplyDefaults = this.state.status === undefined &&
12115
+ this.state.optimisticStatus === undefined;
12116
+ let persistedPreferencesApplied = false;
12117
+ if (shouldApplyDefaults && this.devicePersistence.enabled) {
12118
+ persistedPreferencesApplied = await this.applyPersistedPreferences(true);
12119
+ }
12120
+ const canPublish = this.call.permissionsContext.canPublish(this.trackType);
12121
+ if (shouldApplyDefaults && !persistedPreferencesApplied) {
12002
12122
  if (canPublish && settings.mic_default_on) {
12003
12123
  await this.enable();
12004
12124
  }
@@ -12149,7 +12269,7 @@ class ScreenShareState extends AudioDeviceManagerState {
12149
12269
 
12150
12270
  class ScreenShareManager extends AudioDeviceManager {
12151
12271
  constructor(call) {
12152
- super(call, new ScreenShareState(), TrackType.SCREEN_SHARE);
12272
+ super(call, new ScreenShareState(), TrackType.SCREEN_SHARE, normalize({ enabled: false }));
12153
12273
  }
12154
12274
  setup() {
12155
12275
  if (this.areSubscriptionsSetUp)
@@ -12298,7 +12418,7 @@ class SpeakerState {
12298
12418
  }
12299
12419
 
12300
12420
  class SpeakerManager {
12301
- constructor(call) {
12421
+ constructor(call, devicePreferences) {
12302
12422
  this.subscriptions = [];
12303
12423
  this.areSubscriptionsSetUp = false;
12304
12424
  /**
@@ -12307,18 +12427,35 @@ class SpeakerManager {
12307
12427
  * @internal
12308
12428
  */
12309
12429
  this.dispose = () => {
12310
- this.subscriptions.forEach((s) => s.unsubscribe());
12430
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
12311
12431
  this.subscriptions = [];
12312
12432
  this.areSubscriptionsSetUp = false;
12313
12433
  };
12314
12434
  this.call = call;
12315
12435
  this.state = new SpeakerState(call.tracer);
12436
+ this.devicePersistence = devicePreferences;
12316
12437
  this.setup();
12317
12438
  }
12318
12439
  apply(settings) {
12319
- if (!isReactNative()) {
12440
+ return isReactNative() ? this.applyRN(settings) : this.applyWeb();
12441
+ }
12442
+ applyWeb() {
12443
+ const { enabled, storageKey } = this.devicePersistence;
12444
+ if (!enabled)
12320
12445
  return;
12446
+ const preferences = readPreferences(storageKey);
12447
+ const preferenceList = toPreferenceList(preferences.speaker);
12448
+ if (preferenceList.length === 0)
12449
+ return;
12450
+ const preference = preferenceList[0];
12451
+ const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
12452
+ ? ''
12453
+ : preference.selectedDeviceId;
12454
+ if (this.state.selectedDevice !== nextDeviceId) {
12455
+ this.select(nextDeviceId);
12321
12456
  }
12457
+ }
12458
+ applyRN(settings) {
12322
12459
  /// Determines if the speaker should be enabled based on a priority hierarchy of
12323
12460
  /// settings.
12324
12461
  ///
@@ -12347,19 +12484,21 @@ class SpeakerManager {
12347
12484
  }
12348
12485
  }
12349
12486
  setup() {
12350
- if (this.areSubscriptionsSetUp) {
12487
+ if (this.areSubscriptionsSetUp)
12351
12488
  return;
12352
- }
12353
12489
  this.areSubscriptionsSetUp = true;
12354
12490
  if (deviceIds$ && !isReactNative()) {
12355
- this.subscriptions.push(combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
12356
- if (!deviceId) {
12491
+ this.subscriptions.push(createSubscription(combineLatest([deviceIds$, this.state.selectedDevice$]), ([devices, deviceId]) => {
12492
+ if (!deviceId)
12357
12493
  return;
12358
- }
12359
12494
  const device = devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
12360
- if (!device) {
12495
+ if (!device)
12361
12496
  this.select('');
12362
- }
12497
+ }));
12498
+ }
12499
+ if (!isReactNative() && this.devicePersistence.enabled) {
12500
+ this.subscriptions.push(createSubscription(this.state.selectedDevice$, (selectedDevice) => {
12501
+ this.persistSpeakerDevicePreference(selectedDevice);
12363
12502
  }));
12364
12503
  }
12365
12504
  }
@@ -12419,6 +12558,13 @@ class SpeakerManager {
12419
12558
  return { audioVolume: volume };
12420
12559
  });
12421
12560
  }
12561
+ persistSpeakerDevicePreference(selectedDevice) {
12562
+ const { storageKey } = this.devicePersistence;
12563
+ const devices = getCurrentValue(this.listDevices()) || [];
12564
+ const currentDevice = devices.find((d) => d.deviceId === selectedDevice) ??
12565
+ createSyntheticDevice(selectedDevice, 'audiooutput');
12566
+ writePreferences(currentDevice, 'speaker', undefined, storageKey);
12567
+ }
12422
12568
  }
12423
12569
  const assertUnsupportedInReactNative = () => {
12424
12570
  if (isReactNative()) {
@@ -14523,9 +14669,10 @@ class Call {
14523
14669
  this.state.setMembers(members || []);
14524
14670
  this.state.setOwnCapabilities(ownCapabilities || []);
14525
14671
  this.state.setCallingState(ringing ? CallingState.RINGING : CallingState.IDLE);
14526
- this.camera = new CameraManager(this);
14527
- this.microphone = new MicrophoneManager(this);
14528
- this.speaker = new SpeakerManager(this);
14672
+ const preferences = normalize(streamClient.options.devicePersistence);
14673
+ this.camera = new CameraManager(this, preferences);
14674
+ this.microphone = new MicrophoneManager(this, preferences);
14675
+ this.speaker = new SpeakerManager(this, preferences);
14529
14676
  this.screenShare = new ScreenShareManager(this);
14530
14677
  this.dynascaleManager = new DynascaleManager(this.state, this.speaker, this.tracer);
14531
14678
  }
@@ -15669,7 +15816,7 @@ class StreamClient {
15669
15816
  this.getUserAgent = () => {
15670
15817
  if (!this.cachedUserAgent) {
15671
15818
  const { clientAppIdentifier = {} } = this.options;
15672
- const { sdkName = 'js', sdkVersion = "1.43.0", ...extras } = clientAppIdentifier;
15819
+ const { sdkName = 'js', sdkVersion = "1.44.0", ...extras } = clientAppIdentifier;
15673
15820
  this.cachedUserAgent = [
15674
15821
  `stream-video-${sdkName}-v${sdkVersion}`,
15675
15822
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),