@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
@@ -0,0 +1,106 @@
1
+ import { isReactNative } from '../helpers/platforms';
2
+ import { videoLoggerSystem } from '../logger';
3
+
4
+ export type DevicePersistenceOptions = {
5
+ /**
6
+ * Enables device preference persistence on web.
7
+ * @default true
8
+ */
9
+ enabled?: boolean;
10
+ /**
11
+ * Storage key for persisted preferences.
12
+ * @default '@stream-io/device-preferences'
13
+ */
14
+ storageKey?: string;
15
+ };
16
+
17
+ export type DevicePreferenceKey = 'microphone' | 'camera' | 'speaker';
18
+
19
+ export type LocalDevicePreference = {
20
+ selectedDeviceId: string;
21
+ selectedDeviceLabel: string;
22
+ muted?: boolean;
23
+ };
24
+
25
+ export type LocalDevicePreferences = {
26
+ [type in DevicePreferenceKey]?:
27
+ | LocalDevicePreference
28
+ | LocalDevicePreference[];
29
+ };
30
+
31
+ export const defaultDeviceId = 'default';
32
+
33
+ const isLocalStorageAvailable = (): boolean =>
34
+ typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
35
+
36
+ export const normalize = (
37
+ options: DevicePersistenceOptions | undefined,
38
+ ): Required<DevicePersistenceOptions> => {
39
+ return {
40
+ storageKey: options?.storageKey ?? `@stream-io/device-preferences`,
41
+ enabled:
42
+ isLocalStorageAvailable() && !isReactNative()
43
+ ? (options?.enabled ?? true)
44
+ : false,
45
+ };
46
+ };
47
+
48
+ export const createSyntheticDevice = (
49
+ deviceId: string,
50
+ kind: MediaDeviceKind,
51
+ ): MediaDeviceInfo => {
52
+ return { deviceId, kind, label: '', groupId: '' } as MediaDeviceInfo;
53
+ };
54
+
55
+ export const readPreferences = (storageKey: string): LocalDevicePreferences => {
56
+ try {
57
+ const raw = window.localStorage.getItem(storageKey) || '{}';
58
+ return JSON.parse(raw) as LocalDevicePreferences;
59
+ } catch {
60
+ return {};
61
+ }
62
+ };
63
+
64
+ export const writePreferences = (
65
+ currentDevice: MediaDeviceInfo | undefined,
66
+ deviceKey: DevicePreferenceKey,
67
+ muted: boolean | undefined,
68
+ storageKey: string,
69
+ ) => {
70
+ if (!isLocalStorageAvailable()) return;
71
+
72
+ const selectedDeviceId = currentDevice?.deviceId ?? defaultDeviceId;
73
+ const selectedDeviceLabel = currentDevice?.label ?? '';
74
+
75
+ const preferences = readPreferences(storageKey);
76
+ const preferenceHistory = [preferences[deviceKey] ?? []]
77
+ .flat()
78
+ .filter(
79
+ (p) =>
80
+ p.selectedDeviceId !== selectedDeviceId &&
81
+ (p.selectedDeviceLabel === '' ||
82
+ p.selectedDeviceLabel !== selectedDeviceLabel),
83
+ );
84
+
85
+ const nextPreferences: LocalDevicePreferences = {
86
+ ...preferences,
87
+ [deviceKey]: [
88
+ {
89
+ selectedDeviceId,
90
+ selectedDeviceLabel,
91
+ ...(typeof muted === 'boolean' ? { muted } : {}),
92
+ } satisfies LocalDevicePreference,
93
+ ...preferenceHistory,
94
+ ].slice(0, 3),
95
+ };
96
+ try {
97
+ window.localStorage.setItem(storageKey, JSON.stringify(nextPreferences));
98
+ } catch (err) {
99
+ const logger = videoLoggerSystem.getLogger('DevicePersistence');
100
+ logger.error('failed to save device preferences', err);
101
+ }
102
+ };
103
+
104
+ export const toPreferenceList = (
105
+ preference?: LocalDevicePreference | LocalDevicePreference[],
106
+ ): LocalDevicePreference[] => (preference ? [preference].flat() : []);
@@ -134,7 +134,7 @@ export const getAudioDevices = lazy((tracer?: Tracer) => {
134
134
  getDeviceChangeObserver(tracer),
135
135
  getAudioBrowserPermission().asObservable(),
136
136
  ).pipe(
137
- startWith(undefined),
137
+ startWith([]),
138
138
  concatMap(() =>
139
139
  getDevices(getAudioBrowserPermission(), 'audioinput', tracer),
140
140
  ),
@@ -153,7 +153,7 @@ export const getVideoDevices = lazy((tracer?: Tracer) => {
153
153
  getDeviceChangeObserver(tracer),
154
154
  getVideoBrowserPermission().asObservable(),
155
155
  ).pipe(
156
- startWith(undefined),
156
+ startWith([]),
157
157
  concatMap(() =>
158
158
  getDevices(getVideoBrowserPermission(), 'videoinput', tracer),
159
159
  ),
@@ -172,7 +172,7 @@ export const getAudioOutputDevices = lazy((tracer?: Tracer) => {
172
172
  getDeviceChangeObserver(tracer),
173
173
  getAudioBrowserPermission().asObservable(),
174
174
  ).pipe(
175
- startWith(undefined),
175
+ startWith([]),
176
176
  concatMap(() =>
177
177
  getDevices(getAudioBrowserPermission(), 'audiooutput', tracer),
178
178
  ),
@@ -30,7 +30,9 @@ describe('DynascaleManager', () => {
30
30
  call = new Call({
31
31
  id: 'id',
32
32
  type: 'default',
33
- streamClient: new StreamClient('api-key'),
33
+ streamClient: new StreamClient('api-key', {
34
+ devicePersistence: { enabled: false },
35
+ }),
34
36
  clientStore: new StreamVideoWriteableStateStore(),
35
37
  });
36
38
  call.setSortParticipantsBy(noopComparator());
@@ -7,7 +7,7 @@ import type {
7
7
  import { StreamClient } from '../coordinator/connection/client';
8
8
  import { getSdkInfo } from './client-details';
9
9
  import { SdkType } from '../gen/video/sfu/models/models';
10
- import type { StreamVideoClientOptions } from '../StreamVideoClient';
10
+ import type { StreamVideoClientOptions } from '../types';
11
11
 
12
12
  /**
13
13
  * Utility function to get the instance key.
@@ -58,6 +58,7 @@ export class PermissionsContext {
58
58
  return false;
59
59
  default:
60
60
  ensureExhausted(trackType, 'Unknown track type');
61
+ return false;
61
62
  }
62
63
  };
63
64
 
@@ -102,7 +102,7 @@ export const paginatedLayoutSortPreset = combineComparators(
102
102
  * The sorting preset for livestreams and audio rooms.
103
103
  */
104
104
  export const livestreamOrAudioRoomSortPreset = combineComparators(
105
- ifInvisibleBy(
105
+ ifInvisibleOrUnknownBy(
106
106
  combineComparators(
107
107
  dominantSpeaker,
108
108
  speaking,
@@ -1042,7 +1042,7 @@ export class CallState {
1042
1042
  ) => {
1043
1043
  const participant = this.findParticipantBySessionId(sessionId);
1044
1044
  if (!participant) {
1045
- this.logger.warn(`Participant with sessionId ${sessionId} not found`);
1045
+ this.logger.debug(`Participant with sessionId ${sessionId} not found`);
1046
1046
  return;
1047
1047
  }
1048
1048
 
package/src/types.ts CHANGED
@@ -4,20 +4,25 @@ import type {
4
4
  VideoDimension,
5
5
  } from './gen/video/sfu/models/models';
6
6
  import type {
7
+ AudioSettingsRequestDefaultDeviceEnum,
7
8
  CallRecordingStartedEventRecordingTypeEnum,
8
9
  JoinCallRequest,
9
10
  MemberResponse,
10
11
  OwnCapability,
11
12
  ReactionResponse,
12
- AudioSettingsRequestDefaultDeviceEnum,
13
13
  StartRecordingRequest,
14
14
  StartRecordingResponse,
15
15
  } from './gen/coordinator';
16
16
  import type { StreamClient } from './coordinator/connection/client';
17
+ import type {
18
+ RejectReason,
19
+ StreamClientOptions,
20
+ TokenProvider,
21
+ User,
22
+ } from './coordinator/connection/types';
17
23
  import type { Comparator } from './sorting';
18
24
  import type { StreamVideoWriteableStateStore } from './store';
19
25
  import { AxiosError } from 'axios';
20
- import { RejectReason } from './coordinator/connection/types';
21
26
 
22
27
  export type StreamReaction = Pick<
23
28
  ReactionResponse,
@@ -334,6 +339,48 @@ export type CallConstructor = {
334
339
  clientStore: StreamVideoWriteableStateStore;
335
340
  };
336
341
 
342
+ type StreamVideoClientBaseOptions = {
343
+ apiKey: string;
344
+ options?: StreamClientOptions;
345
+ };
346
+
347
+ type StreamVideoClientOptionsWithoutUser = StreamVideoClientBaseOptions & {
348
+ user?: undefined;
349
+ token?: never;
350
+ tokenProvider?: never;
351
+ };
352
+
353
+ type GuestUser = Extract<User, { type: 'guest' }>;
354
+ type AnonymousUser = Extract<User, { type: 'anonymous' }>;
355
+ type AuthenticatedUser = Exclude<User, GuestUser | AnonymousUser>;
356
+
357
+ type StreamVideoClientOptionsWithGuestUser = StreamVideoClientBaseOptions & {
358
+ user: GuestUser;
359
+ token?: never;
360
+ tokenProvider?: never;
361
+ };
362
+
363
+ type StreamVideoClientOptionsWithAnonymousUser =
364
+ StreamVideoClientBaseOptions & {
365
+ user: AnonymousUser;
366
+ token?: string;
367
+ tokenProvider?: TokenProvider;
368
+ };
369
+
370
+ type StreamVideoClientOptionsWithAuthenticatedUser =
371
+ StreamVideoClientBaseOptions & {
372
+ user: AuthenticatedUser;
373
+ } & (
374
+ | { token: string; tokenProvider?: TokenProvider }
375
+ | { token?: string; tokenProvider: TokenProvider }
376
+ );
377
+
378
+ export type StreamVideoClientOptions =
379
+ | StreamVideoClientOptionsWithoutUser
380
+ | StreamVideoClientOptionsWithGuestUser
381
+ | StreamVideoClientOptionsWithAnonymousUser
382
+ | StreamVideoClientOptionsWithAuthenticatedUser;
383
+
337
384
  export type CallRecordingType = CallRecordingStartedEventRecordingTypeEnum;
338
385
  export type StartCallRecordingFnType = {
339
386
  (): Promise<StartRecordingResponse>;