@stream-io/video-react-sdk 1.12.11 → 1.13.1

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.
package/dist/index.cjs.js CHANGED
@@ -75,135 +75,122 @@ const useFloatingUIPreset = ({ middleware = [], placement, strategy, offset: off
75
75
 
76
76
  const defaultDevice = 'default';
77
77
  /**
78
- * This hook will persist the device settings to local storage.
78
+ * This hook will apply and persist the device preferences from local storage.
79
79
  *
80
80
  * @param key the key to use for local storage.
81
81
  */
82
- const usePersistDevicePreferences = (key, shouldPersistRef) => {
83
- const { useMicrophoneState, useCameraState, useSpeakerState } = videoReactBindings.useCallStateHooks();
84
- const call = videoReactBindings.useCall();
85
- const mic = useMicrophoneState();
86
- const camera = useCameraState();
87
- const speaker = useSpeakerState();
88
- react.useEffect(() => {
89
- if (!shouldPersistRef.current)
90
- return;
91
- if (!call)
82
+ const usePersistedDevicePreferences = (key = '@stream-io/device-preferences') => {
83
+ const { useCameraState, useMicrophoneState, useSpeakerState } = videoReactBindings.useCallStateHooks();
84
+ usePersistedDevicePreference(key, 'camera', useCameraState());
85
+ usePersistedDevicePreference(key, 'microphone', useMicrophoneState());
86
+ usePersistedDevicePreference(key, 'speaker', useSpeakerState());
87
+ };
88
+ const usePersistedDevicePreference = (key, deviceKey, state) => {
89
+ const { useCallCallingState } = videoReactBindings.useCallStateHooks();
90
+ const callingState = useCallCallingState();
91
+ const [applyingState, setApplyingState] = react.useState('idle');
92
+ const manager = state[deviceKey];
93
+ react.useEffect(function apply() {
94
+ if (callingState === videoClient.CallingState.LEFT ||
95
+ !state.devices?.length ||
96
+ applyingState !== 'idle') {
92
97
  return;
93
- if (call.state.callingState === videoClient.CallingState.LEFT)
98
+ }
99
+ const preferences = parseLocalDevicePreferences(key);
100
+ const preference = preferences[deviceKey];
101
+ setApplyingState('applying');
102
+ if (preference && !manager.state.selectedDevice) {
103
+ selectDevice(manager, [preference].flat(), state.devices)
104
+ .catch((err) => {
105
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
106
+ })
107
+ .finally(() => setApplyingState('applied'));
108
+ }
109
+ else {
110
+ setApplyingState('applied');
111
+ }
112
+ }, [applyingState, callingState, deviceKey, key, manager, state.devices]);
113
+ react.useEffect(function persist() {
114
+ if (callingState === videoClient.CallingState.LEFT ||
115
+ !state.devices?.length ||
116
+ applyingState !== 'applied') {
94
117
  return;
118
+ }
95
119
  try {
96
- const preferences = {
97
- mic: {
98
- selectedDeviceId: mic.selectedDevice || defaultDevice,
99
- muted: mic.isMute,
100
- },
101
- camera: {
102
- selectedDeviceId: camera.selectedDevice || defaultDevice,
103
- muted: camera.isMute,
104
- },
105
- speaker: {
106
- selectedDeviceId: speaker.selectedDevice || defaultDevice,
107
- muted: false,
108
- },
109
- };
110
- window.localStorage.setItem(key, JSON.stringify(preferences));
120
+ patchLocalDevicePreference(key, deviceKey, {
121
+ devices: state.devices,
122
+ selectedDevice: state.selectedDevice,
123
+ isMute: state.isMute,
124
+ });
111
125
  }
112
126
  catch (err) {
113
- console.warn('Failed to save device preferences', err);
127
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
114
128
  }
115
129
  }, [
116
- call,
117
- camera.isMute,
118
- camera.selectedDevice,
130
+ applyingState,
131
+ callingState,
132
+ deviceKey,
119
133
  key,
120
- mic.isMute,
121
- mic.selectedDevice,
122
- speaker.selectedDevice,
123
- shouldPersistRef,
134
+ state.devices,
135
+ state.isMute,
136
+ state.selectedDevice,
124
137
  ]);
125
138
  };
126
- /**
127
- * This hook will apply the device settings from local storage.
128
- *
129
- * @param key the key to use for local storage.
130
- */
131
- const useApplyDevicePreferences = (key, onApplied) => {
132
- const call = videoReactBindings.useCall();
133
- const onAppliedRef = react.useRef(onApplied);
134
- onAppliedRef.current = onApplied;
135
- react.useEffect(() => {
136
- if (!call)
137
- return;
138
- if (call.state.callingState === videoClient.CallingState.LEFT)
139
- return;
140
- let cancel = false;
141
- const apply = async () => {
142
- const initMic = async (setting) => {
143
- if (cancel)
144
- return;
145
- await call.microphone.select(parseDeviceId(setting.selectedDeviceId));
146
- if (cancel)
147
- return;
148
- if (setting.muted) {
149
- await call.microphone.disable();
150
- }
151
- else {
152
- await call.microphone.enable();
153
- }
154
- };
155
- const initCamera = async (setting) => {
156
- if (cancel)
157
- return;
158
- await call.camera.select(parseDeviceId(setting.selectedDeviceId));
159
- if (cancel)
160
- return;
161
- if (setting.muted) {
162
- await call.camera.disable();
163
- }
164
- else {
165
- await call.camera.enable();
166
- }
167
- };
168
- const initSpeaker = (setting) => {
169
- if (cancel)
170
- return;
171
- call.speaker.select(parseDeviceId(setting.selectedDeviceId) ?? '');
172
- };
173
- let preferences = null;
174
- try {
175
- preferences = JSON.parse(window.localStorage.getItem(key));
176
- }
177
- catch (err) {
178
- console.warn('Failed to load device preferences', err);
179
- }
180
- if (preferences) {
181
- await initMic(preferences.mic);
182
- await initCamera(preferences.camera);
183
- initSpeaker(preferences.speaker);
139
+ const parseLocalDevicePreferences = (key) => {
140
+ const preferencesStr = window.localStorage.getItem(key);
141
+ let preferences = {};
142
+ if (preferencesStr) {
143
+ try {
144
+ preferences = JSON.parse(preferencesStr);
145
+ if (Object.hasOwn(preferences, 'mic')) {
146
+ // for backwards compatibility
147
+ preferences.microphone = preferences.mic;
184
148
  }
185
- };
186
- apply()
187
- .then(() => onAppliedRef.current())
188
- .catch((err) => {
189
- console.warn('Failed to apply device preferences', err);
190
- });
191
- return () => {
192
- cancel = true;
193
- };
194
- }, [call, key]);
149
+ }
150
+ catch {
151
+ /* assume preferences are empty */
152
+ }
153
+ }
154
+ return preferences;
155
+ };
156
+ const patchLocalDevicePreference = (key, deviceKey, state) => {
157
+ const preferences = parseLocalDevicePreferences(key);
158
+ const nextPreference = getSelectedDevicePreference(state.devices, state.selectedDevice);
159
+ const preferenceHistory = [preferences[deviceKey] ?? []]
160
+ .flat()
161
+ .filter((p) => p.selectedDeviceId !== nextPreference.selectedDeviceId &&
162
+ (p.selectedDeviceLabel === '' ||
163
+ p.selectedDeviceLabel !== nextPreference.selectedDeviceLabel));
164
+ window.localStorage.setItem(key, JSON.stringify({
165
+ ...preferences,
166
+ mic: undefined, // for backwards compatibility
167
+ [deviceKey]: [
168
+ {
169
+ ...nextPreference,
170
+ muted: state.isMute,
171
+ },
172
+ ...preferenceHistory,
173
+ ].slice(0, 3),
174
+ }));
195
175
  };
196
- /**
197
- * This hook will apply and persist the device preferences from local storage.
198
- *
199
- * @param key the key to use for local storage.
200
- */
201
- const usePersistedDevicePreferences = (key = '@stream-io/device-preferences') => {
202
- const shouldPersistRef = react.useRef(false);
203
- useApplyDevicePreferences(key, () => (shouldPersistRef.current = true));
204
- usePersistDevicePreferences(key, shouldPersistRef);
176
+ const selectDevice = async (manager, preference, devices) => {
177
+ for (const p of preference) {
178
+ if (p.selectedDeviceId === defaultDevice) {
179
+ return;
180
+ }
181
+ const device = devices.find((d) => d.deviceId === p.selectedDeviceId) ??
182
+ devices.find((d) => d.label === p.selectedDeviceLabel);
183
+ if (device) {
184
+ await manager.select(device.deviceId);
185
+ await manager[p.muted ? 'disable' : 'enable']?.();
186
+ return;
187
+ }
188
+ }
205
189
  };
206
- const parseDeviceId = (deviceId) => deviceId !== defaultDevice ? deviceId : undefined;
190
+ const getSelectedDevicePreference = (devices, selectedDevice) => ({
191
+ selectedDeviceId: selectedDevice || defaultDevice,
192
+ selectedDeviceLabel: devices?.find((d) => d.deviceId === selectedDevice)?.label ?? '',
193
+ });
207
194
 
208
195
  const SCROLL_THRESHOLD = 10;
209
196
  /**
@@ -673,7 +660,7 @@ const Video$1 = ({ enabled = true, mirror, trackType, participant, className, Vi
673
660
  }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
674
661
  setVideoElement(element);
675
662
  refs?.setVideoElement?.(element);
676
- } })), isPiP && (jsxRuntime.jsx(DefaultPictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
663
+ } })), isPiP && PictureInPicturePlaceholder && (jsxRuntime.jsx(PictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
677
664
  };
678
665
  Video$1.displayName = 'Video';
679
666
 
@@ -838,8 +825,10 @@ const useRenderer = (tfLite) => {
838
825
  output,
839
826
  stop: () => {
840
827
  renderer?.dispose();
841
- videoRef.current && (videoRef.current.srcObject = null);
842
- outputStream && videoClient.disposeOfMediaStream(outputStream);
828
+ if (videoRef.current)
829
+ videoRef.current.srcObject = null;
830
+ if (outputStream)
831
+ videoClient.disposeOfMediaStream(outputStream);
843
832
  },
844
833
  };
845
834
  }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
@@ -1033,7 +1022,7 @@ const RecordCallConfirmationButton = ({ caption, }) => {
1033
1022
  }
1034
1023
  const title = isAwaitingResponse
1035
1024
  ? t('Waiting for recording to start...')
1036
- : caption ?? t('Record call');
1025
+ : (caption ?? t('Record call'));
1037
1026
  return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [
1038
1027
  videoClient.OwnCapability.START_RECORD_CALL,
1039
1028
  videoClient.OwnCapability.STOP_RECORD_CALL,
@@ -1284,7 +1273,7 @@ const DeviceSelectorDropdown = (props) => {
1284
1273
  return (jsxRuntime.jsxs("div", { className: "str-video__device-settings__device-kind", children: [jsxRuntime.jsx("div", { className: "str-video__device-settings__device-selector-title", children: title }), jsxRuntime.jsx(DropDownSelect, { icon: icon, defaultSelectedIndex: selectedIndex, defaultSelectedLabel: selectedDeviceInfo.label, handleSelect: handleSelect, children: deviceList.map((device) => (jsxRuntime.jsx(DropDownSelectOption, { icon: icon, label: device.label, selected: device.isSelected }, device.deviceId))) })] }));
1285
1274
  };
1286
1275
  const DeviceSelector = (props) => {
1287
- const { visualType = 'list', icon, placeholder, ...rest } = props;
1276
+ const { visualType = 'list', icon, ...rest } = props;
1288
1277
  if (visualType === 'list') {
1289
1278
  return jsxRuntime.jsx(DeviceSelectorList, { ...rest });
1290
1279
  }
@@ -1339,7 +1328,7 @@ const ToggleAudioPreviewButton = (props) => {
1339
1328
  const handleClick = createCallControlHandler(props, () => microphone.toggle());
1340
1329
  return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1341
1330
  ? t('Check your browser audio permissions')
1342
- : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1331
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1343
1332
  ? 'preview-audio-unmute-button'
1344
1333
  : 'preview-audio-mute-button', onClick: handleClick, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1345
1334
  setTooltipDisabled(shown);
@@ -1365,7 +1354,7 @@ const ToggleAudioPublishingButton = (props) => {
1365
1354
  ? t('You have no permission to share your audio')
1366
1355
  : !hasBrowserPermission
1367
1356
  ? t('Check your browser mic permissions')
1368
- : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1357
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission || !hasPermission, "data-testid": optimisticIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1369
1358
  setTooltipDisabled(shown);
1370
1359
  onMenuToggle?.(shown);
1371
1360
  }, children: [jsxRuntime.jsx(Icon, { icon: optimisticIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }) }) }));
@@ -1380,7 +1369,7 @@ const ToggleVideoPreviewButton = (props) => {
1380
1369
  const handleClick = createCallControlHandler(props, () => camera.toggle());
1381
1370
  return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
1382
1371
  ? t('Check your browser video permissions')
1383
- : caption ?? t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1372
+ : (caption ?? t('Video')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1384
1373
  ? 'preview-video-unmute-button'
1385
1374
  : 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1386
1375
  setTooltipDisabled(shown);
@@ -1690,7 +1679,7 @@ const SearchInput = ({ exitSearch, isActive, ...rest }) => {
1690
1679
  }), children: [jsxRuntime.jsx("input", { placeholder: "Search", ...rest, ref: setInputElement }), isActive ? (jsxRuntime.jsx("button", { className: "str-video__search-input__clear-btn", onClick: exitSearch, children: jsxRuntime.jsx("span", { className: "str-video__search-input__icon--active" }) })) : (jsxRuntime.jsx("span", { className: "str-video__search-input__icon" }))] }));
1691
1680
  };
1692
1681
 
1693
- const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) => {
1682
+ function SearchResults({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) {
1694
1683
  if (searchQueryInProgress) {
1695
1684
  return (jsxRuntime.jsx("div", { className: "str-video__search-results--loading", children: jsxRuntime.jsx(LoadingIndicator$1, {}) }));
1696
1685
  }
@@ -1698,7 +1687,7 @@ const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIn
1698
1687
  return jsxRuntime.jsx(EmptySearchResultComponent, {});
1699
1688
  }
1700
1689
  return jsxRuntime.jsx(SearchResultList, { data: searchResults });
1701
- };
1690
+ }
1702
1691
 
1703
1692
  const useSearch = ({ debounceInterval, searchFn, searchQuery = '', }) => {
1704
1693
  const [searchResults, setSearchResults] = react.useState([]);
@@ -2309,7 +2298,7 @@ function checkConditions(obj, conditions) {
2309
2298
  for (const key of Object.keys(conditions)) {
2310
2299
  const operator = conditions[key];
2311
2300
  const maybeOperator = operator && typeof operator === 'object';
2312
- let value = obj[key];
2301
+ const value = obj[key];
2313
2302
  if (maybeOperator && '$eq' in operator) {
2314
2303
  const eqOperator = operator;
2315
2304
  match && (match = eqOperator.$eq === value);
@@ -2680,7 +2669,7 @@ const LivestreamPlayer = (props) => {
2680
2669
  return (jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(LivestreamLayout, { ...layoutProps }) }));
2681
2670
  };
2682
2671
 
2683
- const [major, minor, patch] = ("1.12.11").split('.');
2672
+ const [major, minor, patch] = ("1.13.1").split('.');
2684
2673
  videoClient.setSdkInfo({
2685
2674
  type: videoClient.SfuModels.SdkType.REACT,
2686
2675
  major,