@stream-io/video-react-sdk 1.12.10 → 1.13.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.
@@ -7,5 +7,4 @@ export declare const DeviceSelector: (props: {
7
7
  title?: string;
8
8
  onChange?: (deviceId: string) => void;
9
9
  visualType?: "list" | "dropdown";
10
- placeholder?: string;
11
10
  }) => import("react/jsx-runtime").JSX.Element;
@@ -11,4 +11,4 @@ export type SearchResultsProps<T> = Pick<SearchController<T>, 'searchResults' |
11
11
  /** Component to be displayed while the search query request is in progress */
12
12
  LoadingIndicator?: ComponentType;
13
13
  };
14
- export declare const SearchResults: <T extends unknown>({ EmptySearchResultComponent, LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }: SearchResultsProps<T>) => import("react/jsx-runtime").JSX.Element;
14
+ export declare function SearchResults<T>({ EmptySearchResultComponent, LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }: SearchResultsProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,12 @@
1
- export declare const useCalculateHardLimit: (wrapperElement: HTMLDivElement | null, hostElement: HTMLDivElement | null, limit?: "dynamic" | number) => {
1
+ export declare const useCalculateHardLimit: (
2
+ /**
3
+ * Element that stretches to 100% of the whole layout component
4
+ */
5
+ wrapperElement: HTMLDivElement | null,
6
+ /**
7
+ * Element that directly hosts individual `ParticipantView` (or wrapper) elements
8
+ */
9
+ hostElement: HTMLDivElement | null, limit?: "dynamic" | number) => {
2
10
  vertical: number | null;
3
11
  horizontal: number | null;
4
12
  };
@@ -1,13 +1,16 @@
1
1
  export type LocalDevicePreference = {
2
2
  selectedDeviceId: string;
3
- muted: boolean;
3
+ selectedDeviceLabel: string;
4
+ muted?: boolean;
4
5
  };
5
6
  export type LocalDevicePreferences = {
6
- [type in 'mic' | 'camera' | 'speaker']: LocalDevicePreference;
7
+ [type in DeviceKey]?: LocalDevicePreference | LocalDevicePreference[];
7
8
  };
9
+ type DeviceKey = 'microphone' | 'camera' | 'speaker';
8
10
  /**
9
11
  * This hook will apply and persist the device preferences from local storage.
10
12
  *
11
13
  * @param key the key to use for local storage.
12
14
  */
13
15
  export declare const usePersistedDevicePreferences: (key?: string) => void;
16
+ export {};
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.12.10",
4
- "packageManager": "yarn@3.2.4",
3
+ "version": "1.13.0",
5
4
  "main": "./dist/index.cjs.js",
6
5
  "module": "./dist/index.es.js",
7
6
  "types": "./dist/index.d.ts",
@@ -31,9 +30,9 @@
31
30
  ],
32
31
  "dependencies": {
33
32
  "@floating-ui/react": "^0.26.24",
34
- "@stream-io/video-client": "1.18.6",
33
+ "@stream-io/video-client": "1.18.8",
35
34
  "@stream-io/video-filters-web": "0.1.7",
36
- "@stream-io/video-react-bindings": "1.5.8",
35
+ "@stream-io/video-react-bindings": "1.5.10",
37
36
  "chart.js": "^4.4.4",
38
37
  "clsx": "^2.0.0",
39
38
  "react-chartjs-2": "^5.3.0"
@@ -52,8 +51,8 @@
52
51
  "@types/react-dom": "^18.3.0",
53
52
  "react": "^18.3.1",
54
53
  "react-dom": "^18.3.1",
55
- "rimraf": "^5.0.7",
56
- "rollup": "^4.34.7",
57
- "typescript": "^5.5.2"
54
+ "rimraf": "^6.0.1",
55
+ "rollup": "^4.36.0",
56
+ "typescript": "^5.8.2"
58
57
  }
59
58
  }
@@ -321,8 +321,8 @@ const useRenderer = (tfLite: TFLite) => {
321
321
  output,
322
322
  stop: () => {
323
323
  renderer?.dispose();
324
- videoRef.current && (videoRef.current.srcObject = null);
325
- outputStream && disposeOfMediaStream(outputStream);
324
+ if (videoRef.current) videoRef.current.srcObject = null;
325
+ if (outputStream) disposeOfMediaStream(outputStream);
326
326
  },
327
327
  };
328
328
  },
@@ -95,7 +95,7 @@ export const RecordCallConfirmationButton = ({
95
95
 
96
96
  const title = isAwaitingResponse
97
97
  ? t('Waiting for recording to start...')
98
- : caption ?? t('Record call');
98
+ : (caption ?? t('Record call'));
99
99
 
100
100
  return (
101
101
  <Restricted
@@ -13,8 +13,8 @@ import { Icon } from '../Icon';
13
13
  import { WithTooltip } from '../Tooltip';
14
14
  import { useState } from 'react';
15
15
  import {
16
- PropsWithErrorHandler,
17
16
  createCallControlHandler,
17
+ PropsWithErrorHandler,
18
18
  } from '../../utilities/callControlHandler';
19
19
 
20
20
  export type ToggleAudioPreviewButtonProps = PropsWithErrorHandler<
@@ -46,7 +46,7 @@ export const ToggleAudioPreviewButton = (
46
46
  title={
47
47
  !hasBrowserPermission
48
48
  ? t('Check your browser audio permissions')
49
- : caption ?? t('Mic')
49
+ : (caption ?? t('Mic'))
50
50
  }
51
51
  tooltipDisabled={tooltipDisabled}
52
52
  >
@@ -143,7 +143,7 @@ export const ToggleAudioPublishingButton = (
143
143
  ? t('You have no permission to share your audio')
144
144
  : !hasBrowserPermission
145
145
  ? t('Check your browser mic permissions')
146
- : caption ?? t('Mic')
146
+ : (caption ?? t('Mic'))
147
147
  }
148
148
  tooltipDisabled={tooltipDisabled}
149
149
  >
@@ -13,8 +13,8 @@ import { Icon } from '../Icon';
13
13
  import { WithTooltip } from '../Tooltip';
14
14
  import { useState } from 'react';
15
15
  import {
16
- PropsWithErrorHandler,
17
16
  createCallControlHandler,
17
+ PropsWithErrorHandler,
18
18
  } from '../../utilities/callControlHandler';
19
19
 
20
20
  export type ToggleVideoPreviewButtonProps = PropsWithErrorHandler<
@@ -50,7 +50,7 @@ export const ToggleVideoPreviewButton = (
50
50
  title={
51
51
  !hasBrowserPermission
52
52
  ? t('Check your browser video permissions')
53
- : caption ?? t('Video')
53
+ : (caption ?? t('Video'))
54
54
  }
55
55
  tooltipDisabled={tooltipDisabled}
56
56
  >
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { ChangeEventHandler, useCallback } from 'react';
3
3
 
4
- import { useDeviceList } from '../../hooks/useDeviceList';
4
+ import { useDeviceList } from '../../hooks';
5
5
  import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect';
6
6
  import { useMenuContext } from '../Menu';
7
7
 
@@ -147,9 +147,8 @@ export const DeviceSelector = (props: {
147
147
  title?: string;
148
148
  onChange?: (deviceId: string) => void;
149
149
  visualType?: 'list' | 'dropdown';
150
- placeholder?: string;
151
150
  }) => {
152
- const { visualType = 'list', icon, placeholder, ...rest } = props;
151
+ const { visualType = 'list', icon, ...rest } = props;
153
152
 
154
153
  if (visualType === 'list') {
155
154
  return <DeviceSelectorList {...rest} />;
@@ -18,13 +18,13 @@ export type SearchResultsProps<T> = Pick<
18
18
  LoadingIndicator?: ComponentType;
19
19
  };
20
20
 
21
- export const SearchResults = <T extends unknown>({
21
+ export function SearchResults<T>({
22
22
  EmptySearchResultComponent,
23
23
  LoadingIndicator = DefaultLoadingIndicator,
24
24
  searchQueryInProgress,
25
25
  searchResults,
26
26
  SearchResultList,
27
- }: SearchResultsProps<T>) => {
27
+ }: SearchResultsProps<T>) {
28
28
  if (searchQueryInProgress) {
29
29
  return (
30
30
  <div className="str-video__search-results--loading">
@@ -37,4 +37,4 @@ export const SearchResults = <T extends unknown>({
37
37
  }
38
38
 
39
39
  return <SearchResultList data={searchResults} />;
40
- };
40
+ }
@@ -196,8 +196,8 @@ export const Video = ({
196
196
  }}
197
197
  />
198
198
  )}
199
- {isPiP && (
200
- <DefaultPictureInPicturePlaceholder
199
+ {isPiP && PictureInPicturePlaceholder && (
200
+ <PictureInPicturePlaceholder
201
201
  style={{ position: 'absolute' }}
202
202
  participant={participant}
203
203
  />
@@ -1,150 +1,206 @@
1
- import { type MutableRefObject, useEffect, useRef } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { CallingState } from '@stream-io/video-client';
3
- import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
3
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
4
4
 
5
5
  export type LocalDevicePreference = {
6
6
  selectedDeviceId: string;
7
- muted: boolean;
7
+ selectedDeviceLabel: string;
8
+ muted?: boolean;
8
9
  };
9
10
 
10
11
  export type LocalDevicePreferences = {
11
- [type in 'mic' | 'camera' | 'speaker']: LocalDevicePreference;
12
+ // Array is preference history with latest preferences first.
13
+ // Single preference still acceptable for backwards compatibility.
14
+ [type in DeviceKey]?: LocalDevicePreference | LocalDevicePreference[];
12
15
  };
13
16
 
17
+ type DeviceKey = 'microphone' | 'camera' | 'speaker';
18
+
19
+ type DeviceState<K extends DeviceKey> = {
20
+ [ManagerKey in K]: DeviceManagerLike;
21
+ } & {
22
+ isMute?: boolean;
23
+ devices: MediaDeviceInfo[];
24
+ selectedDevice: string | undefined;
25
+ };
26
+
27
+ interface DeviceManagerLike {
28
+ state: { selectedDevice: string | undefined };
29
+ select(deviceId: string): Promise<void> | void;
30
+ }
31
+
14
32
  const defaultDevice = 'default';
15
33
 
16
34
  /**
17
- * This hook will persist the device settings to local storage.
35
+ * This hook will apply and persist the device preferences from local storage.
18
36
  *
19
37
  * @param key the key to use for local storage.
20
38
  */
21
- const usePersistDevicePreferences = (
22
- key: string,
23
- shouldPersistRef: MutableRefObject<boolean>,
24
- ) => {
25
- const { useMicrophoneState, useCameraState, useSpeakerState } =
39
+ export const usePersistedDevicePreferences = (
40
+ key: string = '@stream-io/device-preferences',
41
+ ): void => {
42
+ const { useCameraState, useMicrophoneState, useSpeakerState } =
26
43
  useCallStateHooks();
27
- const call = useCall();
28
- const mic = useMicrophoneState();
29
- const camera = useCameraState();
30
- const speaker = useSpeakerState();
31
- useEffect(() => {
32
- if (!shouldPersistRef.current) return;
33
- if (!call) return;
34
- if (call.state.callingState === CallingState.LEFT) return;
35
- try {
36
- const preferences: LocalDevicePreferences = {
37
- mic: {
38
- selectedDeviceId: mic.selectedDevice || defaultDevice,
39
- muted: mic.isMute,
40
- },
41
- camera: {
42
- selectedDeviceId: camera.selectedDevice || defaultDevice,
43
- muted: camera.isMute,
44
- },
45
- speaker: {
46
- selectedDeviceId: speaker.selectedDevice || defaultDevice,
47
- muted: false,
48
- },
49
- };
50
- window.localStorage.setItem(key, JSON.stringify(preferences));
51
- } catch (err) {
52
- console.warn('Failed to save device preferences', err);
53
- }
54
- }, [
55
- call,
56
- camera.isMute,
57
- camera.selectedDevice,
58
- key,
59
- mic.isMute,
60
- mic.selectedDevice,
61
- speaker.selectedDevice,
62
- shouldPersistRef,
63
- ]);
44
+ usePersistedDevicePreference(key, 'camera', useCameraState());
45
+ usePersistedDevicePreference(key, 'microphone', useMicrophoneState());
46
+ usePersistedDevicePreference(key, 'speaker', useSpeakerState());
64
47
  };
65
48
 
66
- /**
67
- * This hook will apply the device settings from local storage.
68
- *
69
- * @param key the key to use for local storage.
70
- */
71
- const useApplyDevicePreferences = (key: string, onApplied: () => void) => {
72
- const call = useCall();
73
- const onAppliedRef = useRef(onApplied);
74
- onAppliedRef.current = onApplied;
75
- useEffect(() => {
76
- if (!call) return;
77
- if (call.state.callingState === CallingState.LEFT) return;
78
-
79
- let cancel = false;
80
-
81
- const apply = async () => {
82
- const initMic = async (setting: LocalDevicePreference) => {
83
- if (cancel) return;
84
- await call.microphone.select(parseDeviceId(setting.selectedDeviceId));
85
- if (cancel) return;
86
- if (setting.muted) {
87
- await call.microphone.disable();
88
- } else {
89
- await call.microphone.enable();
90
- }
91
- };
92
-
93
- const initCamera = async (setting: LocalDevicePreference) => {
94
- if (cancel) return;
95
- await call.camera.select(parseDeviceId(setting.selectedDeviceId));
96
- if (cancel) return;
97
- if (setting.muted) {
98
- await call.camera.disable();
99
- } else {
100
- await call.camera.enable();
101
- }
102
- };
103
-
104
- const initSpeaker = (setting: LocalDevicePreference) => {
105
- if (cancel) return;
106
- call.speaker.select(parseDeviceId(setting.selectedDeviceId) ?? '');
107
- };
108
-
109
- let preferences: LocalDevicePreferences | null = null;
49
+ const usePersistedDevicePreference = <K extends DeviceKey>(
50
+ key: string,
51
+ deviceKey: K,
52
+ state: DeviceState<K>,
53
+ ): void => {
54
+ const { useCallCallingState } = useCallStateHooks();
55
+ const callingState = useCallCallingState();
56
+ const [applyingState, setApplyingState] = useState<
57
+ 'idle' | 'applying' | 'applied'
58
+ >('idle');
59
+ const manager = state[deviceKey];
60
+
61
+ useEffect(
62
+ function apply() {
63
+ if (
64
+ callingState === CallingState.LEFT ||
65
+ !state.devices?.length ||
66
+ applyingState !== 'idle'
67
+ ) {
68
+ return;
69
+ }
70
+
71
+ const preferences = parseLocalDevicePreferences(key);
72
+ const preference = preferences[deviceKey];
73
+
74
+ setApplyingState('applying');
75
+
76
+ if (preference && !manager.state.selectedDevice) {
77
+ selectDevice(manager, [preference].flat(), state.devices)
78
+ .catch((err) => {
79
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
80
+ })
81
+ .finally(() => setApplyingState('applied'));
82
+ } else {
83
+ setApplyingState('applied');
84
+ }
85
+ },
86
+ [applyingState, callingState, deviceKey, key, manager, state.devices],
87
+ );
88
+
89
+ useEffect(
90
+ function persist() {
91
+ if (
92
+ callingState === CallingState.LEFT ||
93
+ !state.devices?.length ||
94
+ applyingState !== 'applied'
95
+ ) {
96
+ return;
97
+ }
98
+
110
99
  try {
111
- preferences = JSON.parse(
112
- window.localStorage.getItem(key)!,
113
- ) as LocalDevicePreferences;
100
+ patchLocalDevicePreference(key, deviceKey, {
101
+ devices: state.devices,
102
+ selectedDevice: state.selectedDevice,
103
+ isMute: state.isMute,
104
+ });
114
105
  } catch (err) {
115
- console.warn('Failed to load device preferences', err);
106
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
116
107
  }
117
- if (preferences) {
118
- await initMic(preferences.mic);
119
- await initCamera(preferences.camera);
120
- initSpeaker(preferences.speaker);
108
+ },
109
+ [
110
+ applyingState,
111
+ callingState,
112
+ deviceKey,
113
+ key,
114
+ state.devices,
115
+ state.isMute,
116
+ state.selectedDevice,
117
+ ],
118
+ );
119
+ };
120
+
121
+ const parseLocalDevicePreferences = (key: string): LocalDevicePreferences => {
122
+ const preferencesStr = window.localStorage.getItem(key);
123
+ let preferences: LocalDevicePreferences = {};
124
+
125
+ if (preferencesStr) {
126
+ try {
127
+ preferences = JSON.parse(preferencesStr);
128
+
129
+ if (Object.hasOwn(preferences, 'mic')) {
130
+ // for backwards compatibility
131
+ preferences.microphone = (
132
+ preferences as { mic: LocalDevicePreference }
133
+ ).mic;
121
134
  }
122
- };
123
-
124
- apply()
125
- .then(() => onAppliedRef.current())
126
- .catch((err) => {
127
- console.warn('Failed to apply device preferences', err);
128
- });
129
-
130
- return () => {
131
- cancel = true;
132
- };
133
- }, [call, key]);
135
+ } catch {
136
+ /* assume preferences are empty */
137
+ }
138
+ }
139
+
140
+ return preferences;
134
141
  };
135
142
 
136
- /**
137
- * This hook will apply and persist the device preferences from local storage.
138
- *
139
- * @param key the key to use for local storage.
140
- */
141
- export const usePersistedDevicePreferences = (
142
- key: string = '@stream-io/device-preferences',
143
- ) => {
144
- const shouldPersistRef = useRef(false);
145
- useApplyDevicePreferences(key, () => (shouldPersistRef.current = true));
146
- usePersistDevicePreferences(key, shouldPersistRef);
143
+ const patchLocalDevicePreference = (
144
+ key: string,
145
+ deviceKey: DeviceKey,
146
+ state: Pick<DeviceState<never>, 'devices' | 'selectedDevice' | 'isMute'>,
147
+ ): void => {
148
+ const preferences = parseLocalDevicePreferences(key);
149
+ const nextPreference = getSelectedDevicePreference(
150
+ state.devices,
151
+ state.selectedDevice,
152
+ );
153
+ const preferenceHistory = [preferences[deviceKey] ?? []]
154
+ .flat()
155
+ .filter(
156
+ (p) =>
157
+ p.selectedDeviceId !== nextPreference.selectedDeviceId &&
158
+ (p.selectedDeviceLabel === '' ||
159
+ p.selectedDeviceLabel !== nextPreference.selectedDeviceLabel),
160
+ );
161
+
162
+ window.localStorage.setItem(
163
+ key,
164
+ JSON.stringify({
165
+ ...preferences,
166
+ mic: undefined, // for backwards compatibility
167
+ [deviceKey]: [
168
+ {
169
+ ...nextPreference,
170
+ muted: state.isMute,
171
+ } satisfies LocalDevicePreference,
172
+ ...preferenceHistory,
173
+ ].slice(0, 3),
174
+ }),
175
+ );
176
+ };
177
+
178
+ const selectDevice = async (
179
+ manager: DeviceManagerLike,
180
+ preference: LocalDevicePreference[],
181
+ devices: MediaDeviceInfo[],
182
+ ): Promise<void> => {
183
+ for (const p of preference) {
184
+ if (p.selectedDeviceId === defaultDevice) {
185
+ return;
186
+ }
187
+
188
+ const device =
189
+ devices.find((d) => d.deviceId === p.selectedDeviceId) ??
190
+ devices.find((d) => d.label === p.selectedDeviceLabel);
191
+
192
+ if (device) {
193
+ await manager.select(device.deviceId);
194
+ return;
195
+ }
196
+ }
147
197
  };
148
198
 
149
- const parseDeviceId = (deviceId: string) =>
150
- deviceId !== defaultDevice ? deviceId : undefined;
199
+ const getSelectedDevicePreference = (
200
+ devices: MediaDeviceInfo[],
201
+ selectedDevice: string | undefined,
202
+ ): Pick<LocalDevicePreference, 'selectedDeviceId' | 'selectedDeviceLabel'> => ({
203
+ selectedDeviceId: selectedDevice || defaultDevice,
204
+ selectedDeviceLabel:
205
+ devices?.find((d) => d.deviceId === selectedDevice)?.label ?? '',
206
+ });
@@ -45,7 +45,7 @@ function checkConditions<T>(obj: T, conditions: Conditions<T>): boolean {
45
45
  for (const key of Object.keys(conditions) as Array<keyof T>) {
46
46
  const operator = conditions[key];
47
47
  const maybeOperator = operator && typeof operator === 'object';
48
- let value = obj[key];
48
+ const value = obj[key];
49
49
 
50
50
  if (maybeOperator && '$eq' in operator) {
51
51
  const eqOperator = operator as EqOperator<typeof value>;