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