@stream-io/video-client 1.43.0 → 1.44.1-beta.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 (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.browser.es.js +220 -64
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +219 -63
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +220 -64
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +1 -1
  9. package/dist/src/StreamVideoClient.d.ts +2 -8
  10. package/dist/src/coordinator/connection/types.d.ts +5 -0
  11. package/dist/src/devices/CameraManager.d.ts +7 -2
  12. package/dist/src/devices/DeviceManager.d.ts +7 -15
  13. package/dist/src/devices/MicrophoneManager.d.ts +2 -1
  14. package/dist/src/devices/SpeakerManager.d.ts +6 -1
  15. package/dist/src/devices/devicePersistence.d.ts +27 -0
  16. package/dist/src/helpers/clientUtils.d.ts +1 -1
  17. package/dist/src/permissions/PermissionsContext.d.ts +1 -1
  18. package/dist/src/types.d.ts +38 -2
  19. package/package.json +1 -1
  20. package/src/Call.ts +28 -8
  21. package/src/StreamVideoClient.ts +1 -9
  22. package/src/coordinator/connection/types.ts +6 -0
  23. package/src/devices/CameraManager.ts +31 -11
  24. package/src/devices/DeviceManager.ts +113 -31
  25. package/src/devices/MicrophoneManager.ts +26 -8
  26. package/src/devices/ScreenShareManager.ts +7 -1
  27. package/src/devices/SpeakerManager.ts +62 -18
  28. package/src/devices/__tests__/CameraManager.test.ts +184 -21
  29. package/src/devices/__tests__/DeviceManager.test.ts +184 -2
  30. package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -0
  31. package/src/devices/__tests__/MicrophoneManager.test.ts +146 -2
  32. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -0
  33. package/src/devices/__tests__/ScreenShareManager.test.ts +2 -0
  34. package/src/devices/__tests__/SpeakerManager.test.ts +90 -0
  35. package/src/devices/__tests__/devicePersistence.test.ts +142 -0
  36. package/src/devices/__tests__/devices.test.ts +390 -0
  37. package/src/devices/__tests__/mediaStreamTestHelpers.ts +58 -0
  38. package/src/devices/__tests__/mocks.ts +35 -0
  39. package/src/devices/devicePersistence.ts +106 -0
  40. package/src/devices/devices.ts +3 -3
  41. package/src/helpers/__tests__/DynascaleManager.test.ts +3 -1
  42. package/src/helpers/clientUtils.ts +1 -1
  43. package/src/permissions/PermissionsContext.ts +1 -0
  44. package/src/sorting/presets.ts +1 -1
  45. package/src/store/CallState.ts +1 -1
  46. package/src/types.ts +49 -2
@@ -761,7 +761,7 @@ export declare class Call {
761
761
  *
762
762
  * @internal
763
763
  */
764
- applyDeviceConfig: (settings: CallSettingsResponse, publish: boolean) => Promise<void>;
764
+ applyDeviceConfig: (settings: CallSettingsResponse, publish: boolean, skipSpeakerApply?: boolean) => Promise<void>;
765
765
  /**
766
766
  * Will begin tracking the given element for visibility changes within the
767
767
  * configured viewport element (`call.setViewport`).
@@ -2,15 +2,9 @@ import { Call } from './Call';
2
2
  import { StreamClient } from './coordinator/connection/client';
3
3
  import { StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore } from './store';
4
4
  import type { ConnectedEvent, CreateDeviceRequest, CreateGuestRequest, CreateGuestResponse, GetEdgesResponse, ListDevicesResponse, QueryAggregateCallStatsRequest, QueryAggregateCallStatsResponse, QueryCallsRequest, QueryCallStatsRequest, QueryCallStatsResponse } from './gen/coordinator';
5
- import { AllClientEvents, ClientEventListener, StreamClientOptions, TokenOrProvider, TokenProvider, User } from './coordinator/connection/types';
5
+ import { AllClientEvents, ClientEventListener, StreamClientOptions, TokenOrProvider, User } from './coordinator/connection/types';
6
+ import type { StreamVideoClientOptions } from './types';
6
7
  import { ScopedLogger } from './logger';
7
- export type StreamVideoClientOptions = {
8
- apiKey: string;
9
- options?: StreamClientOptions;
10
- user?: User;
11
- token?: string;
12
- tokenProvider?: TokenProvider;
13
- };
14
8
  /**
15
9
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
16
10
  */
@@ -2,6 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
3
3
  import { AllSfuEvents } from '../../rtc';
4
4
  import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
5
+ import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
5
6
  export type UR = Record<string, unknown>;
6
7
  export type User = (Omit<UserRequest, 'role'> & {
7
8
  type?: 'authenticated';
@@ -185,6 +186,10 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
185
186
  * When set to true, the incoming calls are rejected when the user is busy in an another call.
186
187
  */
187
188
  rejectCallWhenBusy?: boolean;
189
+ /**
190
+ * Device persistence preference options (web only).
191
+ */
192
+ devicePersistence?: DevicePersistenceOptions;
188
193
  };
189
194
  export type ClientAppIdentifier = {
190
195
  sdkName?: 'react' | 'react-native' | 'plain-javascript' | (string & {});
@@ -3,21 +3,26 @@ import { Call } from '../Call';
3
3
  import { CameraDirection, CameraManagerState } from './CameraManagerState';
4
4
  import { DeviceManager } from './DeviceManager';
5
5
  import { VideoSettingsResponse } from '../gen/coordinator';
6
+ import { DevicePersistenceOptions } from './devicePersistence';
6
7
  export declare class CameraManager extends DeviceManager<CameraManagerState> {
7
8
  private targetResolution;
8
9
  /**
9
10
  * Constructs a new CameraManager.
10
11
  *
11
12
  * @param call the call instance.
13
+ * @param devicePersistence the device persistence preferences to use.
12
14
  */
13
- constructor(call: Call);
15
+ constructor(call: Call, devicePersistence: Required<DevicePersistenceOptions>);
14
16
  private isDirectionSupportedByDevice;
15
17
  /**
16
18
  * Select the camera direction.
17
19
  *
18
20
  * @param direction the direction of the camera to select.
21
+ * @param options additional direction selection options.
19
22
  */
20
- selectDirection(direction: Exclude<CameraDirection, undefined>): Promise<void>;
23
+ selectDirection(direction: Exclude<CameraDirection, undefined>, options?: {
24
+ enableCamera?: boolean;
25
+ }): Promise<void>;
21
26
  /**
22
27
  * Flips the camera direction: if it's front it will change to back, if it's back, it will change to front.
23
28
  *
@@ -5,6 +5,7 @@ import { DeviceManagerState } from './DeviceManagerState';
5
5
  import { ScopedLogger } from '../logger';
6
6
  import { TrackType } from '../gen/video/sfu/models/models';
7
7
  import { MediaStreamFilter, MediaStreamFilterRegistrationResult } from './filters';
8
+ import { DevicePersistenceOptions } from './devicePersistence';
8
9
  export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C = MediaTrackConstraints> {
9
10
  /**
10
11
  * if true, stops the media stream when call is left
@@ -12,28 +13,16 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
12
13
  stopOnLeave: boolean;
13
14
  logger: ScopedLogger;
14
15
  state: S;
15
- /**
16
- * When `true`, the `apply()` method will skip automatically enabling/disabling
17
- * the device based on server defaults (`mic_default_on`, `camera_default_on`).
18
- *
19
- * This is useful when application code wants to handle device preferences
20
- * (e.g., persisted user preferences) and prevent server defaults from
21
- * overriding them.
22
- *
23
- * @default false
24
- *
25
- * @internal
26
- */
27
- deferServerDefaults: boolean;
28
16
  protected readonly call: Call;
29
17
  protected readonly trackType: TrackType;
30
- protected subscriptions: Function[];
18
+ protected subscriptions: (() => void)[];
19
+ protected devicePersistence: Required<DevicePersistenceOptions>;
31
20
  protected areSubscriptionsSetUp: boolean;
32
21
  private isTrackStoppedDueToTrackEnd;
33
22
  private filters;
34
23
  private statusChangeConcurrencyTag;
35
24
  private filterRegistrationConcurrencyTag;
36
- protected constructor(call: Call, state: S, trackType: TrackType);
25
+ protected constructor(call: Call, state: S, trackType: TrackType, devicePersistence: Required<DevicePersistenceOptions>);
37
26
  setup(): void;
38
27
  /**
39
28
  * Lists the available audio/video devices
@@ -115,4 +104,7 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
115
104
  private get mediaDeviceKind();
116
105
  private handleDisconnectedOrReplacedDevices;
117
106
  protected findDevice(devices: MediaDeviceInfo[], deviceId: string): MediaDeviceInfo | undefined;
107
+ private persistPreference;
108
+ protected applyPersistedPreferences(enabledInCallType: boolean): Promise<boolean>;
109
+ private applyMutedState;
118
110
  }
@@ -6,6 +6,7 @@ import { MicrophoneManagerState } from './MicrophoneManagerState';
6
6
  import { TrackDisableMode } from './DeviceManagerState';
7
7
  import { AudioBitrateProfile } from '../gen/video/sfu/models/models';
8
8
  import { AudioSettingsResponse } from '../gen/coordinator';
9
+ import { DevicePersistenceOptions } from './devicePersistence';
9
10
  export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState> {
10
11
  private speakingWhileMutedNotificationEnabled;
11
12
  private soundDetectorConcurrencyTag;
@@ -18,7 +19,7 @@ export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneMana
18
19
  private noiseCancellationRegistration?;
19
20
  private unregisterNoiseCancellation?;
20
21
  private silenceThresholdMs;
21
- constructor(call: Call, disableMode?: TrackDisableMode);
22
+ constructor(call: Call, devicePersistence: Required<DevicePersistenceOptions>, disableMode?: TrackDisableMode);
22
23
  setup(): void;
23
24
  /**
24
25
  * Enables noise cancellation for the microphone.
@@ -1,14 +1,18 @@
1
1
  import { Call } from '../Call';
2
2
  import { SpeakerState } from './SpeakerState';
3
3
  import { CallSettingsResponse } from '../gen/coordinator';
4
+ import { DevicePersistenceOptions } from './devicePersistence';
4
5
  export declare class SpeakerManager {
5
6
  readonly state: SpeakerState;
6
7
  private subscriptions;
7
8
  private areSubscriptionsSetUp;
8
9
  private readonly call;
9
10
  private defaultDevice?;
10
- constructor(call: Call);
11
+ private readonly devicePersistence;
12
+ constructor(call: Call, devicePreferences: Required<DevicePersistenceOptions>);
11
13
  apply(settings: CallSettingsResponse): void;
14
+ private applyWeb;
15
+ private applyRN;
12
16
  setup(): void;
13
17
  /**
14
18
  * Lists the available audio output devices
@@ -47,4 +51,5 @@ export declare class SpeakerManager {
47
51
  * @param volume a number between 0 and 1. Set it to `undefined` to use the default volume.
48
52
  */
49
53
  setParticipantVolume(sessionId: string, volume: number | undefined): void;
54
+ private persistSpeakerDevicePreference;
50
55
  }
@@ -0,0 +1,27 @@
1
+ export type DevicePersistenceOptions = {
2
+ /**
3
+ * Enables device preference persistence on web.
4
+ * @default true
5
+ */
6
+ enabled?: boolean;
7
+ /**
8
+ * Storage key for persisted preferences.
9
+ * @default '@stream-io/device-preferences'
10
+ */
11
+ storageKey?: string;
12
+ };
13
+ export type DevicePreferenceKey = 'microphone' | 'camera' | 'speaker';
14
+ export type LocalDevicePreference = {
15
+ selectedDeviceId: string;
16
+ selectedDeviceLabel: string;
17
+ muted?: boolean;
18
+ };
19
+ export type LocalDevicePreferences = {
20
+ [type in DevicePreferenceKey]?: LocalDevicePreference | LocalDevicePreference[];
21
+ };
22
+ export declare const defaultDeviceId = "default";
23
+ export declare const normalize: (options: DevicePersistenceOptions | undefined) => Required<DevicePersistenceOptions>;
24
+ export declare const createSyntheticDevice: (deviceId: string, kind: MediaDeviceKind) => MediaDeviceInfo;
25
+ export declare const readPreferences: (storageKey: string) => LocalDevicePreferences;
26
+ export declare const writePreferences: (currentDevice: MediaDeviceInfo | undefined, deviceKey: DevicePreferenceKey, muted: boolean | undefined, storageKey: string) => void;
27
+ export declare const toPreferenceList: (preference?: LocalDevicePreference | LocalDevicePreference[]) => LocalDevicePreference[];
@@ -1,6 +1,6 @@
1
1
  import type { StreamClientOptions, TokenOrProvider, User } from '../coordinator/connection/types';
2
2
  import { StreamClient } from '../coordinator/connection/client';
3
- import type { StreamVideoClientOptions } from '../StreamVideoClient';
3
+ import type { StreamVideoClientOptions } from '../types';
4
4
  /**
5
5
  * Utility function to get the instance key.
6
6
  */
@@ -33,7 +33,7 @@ export declare class PermissionsContext {
33
33
  * Helper method that checks whether the current user has the permission
34
34
  * to publish the given track type.
35
35
  */
36
- canPublish: (trackType: TrackType) => boolean | undefined;
36
+ canPublish: (trackType: TrackType) => boolean;
37
37
  /**
38
38
  * Checks if the current user can request a specific permission
39
39
  * within the call.
@@ -1,10 +1,10 @@
1
1
  import type { Participant, TrackType, VideoDimension } from './gen/video/sfu/models/models';
2
- import type { CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability, ReactionResponse, AudioSettingsRequestDefaultDeviceEnum, StartRecordingRequest, StartRecordingResponse } from './gen/coordinator';
2
+ import type { AudioSettingsRequestDefaultDeviceEnum, CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability, ReactionResponse, StartRecordingRequest, StartRecordingResponse } from './gen/coordinator';
3
3
  import type { StreamClient } from './coordinator/connection/client';
4
+ import type { RejectReason, StreamClientOptions, TokenProvider, User } from './coordinator/connection/types';
4
5
  import type { Comparator } from './sorting';
5
6
  import type { StreamVideoWriteableStateStore } from './store';
6
7
  import { AxiosError } from 'axios';
7
- import { RejectReason } from './coordinator/connection/types';
8
8
  export type StreamReaction = Pick<ReactionResponse, 'type' | 'emoji_code' | 'custom'>;
9
9
  export declare enum VisibilityState {
10
10
  UNKNOWN = "UNKNOWN",
@@ -273,6 +273,42 @@ export type CallConstructor = {
273
273
  */
274
274
  clientStore: StreamVideoWriteableStateStore;
275
275
  };
276
+ type StreamVideoClientBaseOptions = {
277
+ apiKey: string;
278
+ options?: StreamClientOptions;
279
+ };
280
+ type StreamVideoClientOptionsWithoutUser = StreamVideoClientBaseOptions & {
281
+ user?: undefined;
282
+ token?: never;
283
+ tokenProvider?: never;
284
+ };
285
+ type GuestUser = Extract<User, {
286
+ type: 'guest';
287
+ }>;
288
+ type AnonymousUser = Extract<User, {
289
+ type: 'anonymous';
290
+ }>;
291
+ type AuthenticatedUser = Exclude<User, GuestUser | AnonymousUser>;
292
+ type StreamVideoClientOptionsWithGuestUser = StreamVideoClientBaseOptions & {
293
+ user: GuestUser;
294
+ token?: never;
295
+ tokenProvider?: never;
296
+ };
297
+ type StreamVideoClientOptionsWithAnonymousUser = StreamVideoClientBaseOptions & {
298
+ user: AnonymousUser;
299
+ token?: string;
300
+ tokenProvider?: TokenProvider;
301
+ };
302
+ type StreamVideoClientOptionsWithAuthenticatedUser = StreamVideoClientBaseOptions & {
303
+ user: AuthenticatedUser;
304
+ } & ({
305
+ token: string;
306
+ tokenProvider?: TokenProvider;
307
+ } | {
308
+ token?: string;
309
+ tokenProvider: TokenProvider;
310
+ });
311
+ export type StreamVideoClientOptions = StreamVideoClientOptionsWithoutUser | StreamVideoClientOptionsWithGuestUser | StreamVideoClientOptionsWithAnonymousUser | StreamVideoClientOptionsWithAuthenticatedUser;
276
312
  export type CallRecordingType = CallRecordingStartedEventRecordingTypeEnum;
277
313
  export type StartCallRecordingFnType = {
278
314
  (): Promise<StartRecordingResponse>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.43.0",
3
+ "version": "1.44.1-beta.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -162,6 +162,7 @@ import {
162
162
  ScreenShareManager,
163
163
  SpeakerManager,
164
164
  } from './devices';
165
+ import { normalize } from './devices/devicePersistence';
165
166
  import { hasPending, withoutConcurrency } from './helpers/concurrency';
166
167
  import { ensureExhausted } from './helpers/ensureExhausted';
167
168
  import { pushToIfMissing } from './helpers/array';
@@ -171,6 +172,7 @@ import {
171
172
  promiseWithResolvers,
172
173
  } from './helpers/promise';
173
174
  import { GetCallStatsResponse } from './gen/shims';
175
+ import { isReactNative } from './helpers/platforms';
174
176
 
175
177
  /**
176
178
  * An object representation of a `Call`.
@@ -341,9 +343,10 @@ export class Call {
341
343
  ringing ? CallingState.RINGING : CallingState.IDLE,
342
344
  );
343
345
 
344
- this.camera = new CameraManager(this);
345
- this.microphone = new MicrophoneManager(this);
346
- this.speaker = new SpeakerManager(this);
346
+ const preferences = normalize(streamClient.options.devicePersistence);
347
+ this.camera = new CameraManager(this, preferences);
348
+ this.microphone = new MicrophoneManager(this, preferences);
349
+ this.speaker = new SpeakerManager(this, preferences);
347
350
  this.screenShare = new ScreenShareManager(this);
348
351
  this.dynascaleManager = new DynascaleManager(
349
352
  this.state,
@@ -744,7 +747,8 @@ export class Call {
744
747
  // const calls = useCalls().filter((c) => c.ringing);
745
748
  const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
746
749
  this.clientStore.setCalls([this, ...calls]);
747
- await this.applyDeviceConfig(settings, false);
750
+ const skipSpeakerApply = isReactNative() && true;
751
+ await this.applyDeviceConfig(settings, false, skipSpeakerApply);
748
752
  };
749
753
 
750
754
  /**
@@ -779,8 +783,14 @@ export class Call {
779
783
  this.watching = true;
780
784
  this.clientStore.registerOrUpdateCall(this);
781
785
  }
782
-
783
- await this.applyDeviceConfig(response.call.settings, false);
786
+ const skipSpeakerApply = isReactNative()
787
+ ? (params?.ring ?? this.ringing)
788
+ : false;
789
+ await this.applyDeviceConfig(
790
+ response.call.settings,
791
+ false,
792
+ skipSpeakerApply,
793
+ );
784
794
 
785
795
  return response;
786
796
  };
@@ -810,7 +820,14 @@ export class Call {
810
820
  this.clientStore.registerOrUpdateCall(this);
811
821
  }
812
822
 
813
- await this.applyDeviceConfig(response.call.settings, false);
823
+ const skipSpeakerApply = isReactNative()
824
+ ? (data?.ring ?? this.ringing)
825
+ : false;
826
+ await this.applyDeviceConfig(
827
+ response.call.settings,
828
+ false,
829
+ skipSpeakerApply,
830
+ );
814
831
 
815
832
  return response;
816
833
  };
@@ -2794,8 +2811,11 @@ export class Call {
2794
2811
  applyDeviceConfig = async (
2795
2812
  settings: CallSettingsResponse,
2796
2813
  publish: boolean,
2814
+ skipSpeakerApply: boolean = false,
2797
2815
  ) => {
2798
- this.speaker.apply(settings);
2816
+ if (!skipSpeakerApply) {
2817
+ this.speaker.apply(settings);
2818
+ }
2799
2819
  await this.camera.apply(settings.video, publish).catch((err) => {
2800
2820
  this.logger.warn('Camera init failed', err);
2801
2821
  });
@@ -27,10 +27,10 @@ import {
27
27
  ErrorFromResponse,
28
28
  StreamClientOptions,
29
29
  TokenOrProvider,
30
- TokenProvider,
31
30
  User,
32
31
  UserWithId,
33
32
  } from './coordinator/connection/types';
33
+ import type { StreamVideoClientOptions } from './types';
34
34
  import { retryInterval, sleep } from './coordinator/connection/utils';
35
35
  import {
36
36
  createCoordinatorClient,
@@ -42,14 +42,6 @@ import { logToConsole, ScopedLogger, videoLoggerSystem } from './logger';
42
42
  import { withoutConcurrency } from './helpers/concurrency';
43
43
  import { enableTimerWorker } from './timers';
44
44
 
45
- export type StreamVideoClientOptions = {
46
- apiKey: string;
47
- options?: StreamClientOptions;
48
- user?: User;
49
- token?: string;
50
- tokenProvider?: TokenProvider;
51
- };
52
-
53
45
  /**
54
46
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
55
47
  */
@@ -2,6 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
3
3
  import { AllSfuEvents } from '../../rtc';
4
4
  import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
5
+ import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
5
6
 
6
7
  export type UR = Record<string, unknown>;
7
8
 
@@ -258,6 +259,11 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
258
259
  * When set to true, the incoming calls are rejected when the user is busy in an another call.
259
260
  */
260
261
  rejectCallWhenBusy?: boolean;
262
+
263
+ /**
264
+ * Device persistence preference options (web only).
265
+ */
266
+ devicePersistence?: DevicePersistenceOptions;
261
267
  };
262
268
 
263
269
  export type ClientAppIdentifier = {
@@ -7,6 +7,7 @@ import { VideoSettingsResponse } from '../gen/coordinator';
7
7
  import { TrackType } from '../gen/video/sfu/models/models';
8
8
  import { isMobile } from '../helpers/compatibility';
9
9
  import { isReactNative } from '../helpers/platforms';
10
+ import { DevicePersistenceOptions } from './devicePersistence';
10
11
 
11
12
  export class CameraManager extends DeviceManager<CameraManagerState> {
12
13
  private targetResolution = {
@@ -18,9 +19,13 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
18
19
  * Constructs a new CameraManager.
19
20
  *
20
21
  * @param call the call instance.
22
+ * @param devicePersistence the device persistence preferences to use.
21
23
  */
22
- constructor(call: Call) {
23
- super(call, new CameraManagerState(), TrackType.VIDEO);
24
+ constructor(
25
+ call: Call,
26
+ devicePersistence: Required<DevicePersistenceOptions>,
27
+ ) {
28
+ super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
24
29
  }
25
30
 
26
31
  private isDirectionSupportedByDevice() {
@@ -31,8 +36,12 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
31
36
  * Select the camera direction.
32
37
  *
33
38
  * @param direction the direction of the camera to select.
39
+ * @param options additional direction selection options.
34
40
  */
35
- async selectDirection(direction: Exclude<CameraDirection, undefined>) {
41
+ async selectDirection(
42
+ direction: Exclude<CameraDirection, undefined>,
43
+ options: { enableCamera?: boolean } = {},
44
+ ) {
36
45
  if (!this.isDirectionSupportedByDevice()) {
37
46
  this.logger.warn('Setting direction is not supported on this device');
38
47
  return;
@@ -47,9 +56,10 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
47
56
  // providing both device id and direction doesn't work, so we deselect the device
48
57
  this.state.setDirection(direction);
49
58
  this.state.setDevice(undefined);
50
- if (isReactNative()) {
51
- return;
52
- }
59
+
60
+ const { enableCamera = true } = options;
61
+ if (isReactNative() || !enableCamera) return;
62
+
53
63
  this.getTracks().forEach((track) => track.stop());
54
64
  try {
55
65
  await this.unmuteStream();
@@ -120,16 +130,26 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
120
130
  await this.statusChangeSettled();
121
131
  await this.selectTargetResolution(settings.target_resolution);
122
132
 
123
- // apply a direction and enable the camera only if in "pristine" state
124
- // and server defaults are not deferred to application code
133
+ const enabledInCallType = settings.enabled ?? true;
134
+ const shouldApplyDefaults =
135
+ this.state.status === undefined &&
136
+ this.state.optimisticStatus === undefined;
137
+ let persistedPreferencesApplied = false;
138
+ if (shouldApplyDefaults && this.devicePersistence.enabled) {
139
+ persistedPreferencesApplied =
140
+ await this.applyPersistedPreferences(enabledInCallType);
141
+ }
142
+
143
+ // apply a direction and enable the camera only if in "pristine" state,
144
+ // and there are no persisted preferences
125
145
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
126
- if (this.state.status === undefined && !this.deferServerDefaults) {
146
+ if (shouldApplyDefaults && !persistedPreferencesApplied) {
127
147
  if (!this.state.direction && !this.state.selectedDevice) {
128
148
  const direction = settings.camera_facing === 'front' ? 'front' : 'back';
129
- await this.selectDirection(direction);
149
+ await this.selectDirection(direction, { enableCamera: false });
130
150
  }
131
151
 
132
- if (canPublish && settings.camera_default_on && settings.enabled) {
152
+ if (canPublish && settings.camera_default_on && enabledInCallType) {
133
153
  await this.enable();
134
154
  }
135
155
  }