@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.
package/dist/index.es.js CHANGED
@@ -3,7 +3,7 @@ export * from '@stream-io/video-client';
3
3
  import { useCall, useCallStateHooks, useI18n, Restricted, useToggleCallRecording, useConnectedUser, StreamCallProvider, StreamVideoProvider, useStreamVideoClient } from '@stream-io/video-react-bindings';
4
4
  export * from '@stream-io/video-react-bindings';
5
5
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
- import { useState, useEffect, Fragment as Fragment$1, createContext, useContext, useRef, useCallback, useMemo, isValidElement, forwardRef, useLayoutEffect, lazy, Suspense } from 'react';
6
+ import { useState, useEffect, Fragment as Fragment$1, createContext, useContext, useCallback, useMemo, useRef, isValidElement, forwardRef, useLayoutEffect, lazy, Suspense } from 'react';
7
7
  import { useFloating, offset, shift, flip, size, autoUpdate, FloatingOverlay, FloatingPortal, arrow, FloatingArrow, useListItem, useListNavigation, useTypeahead, useClick, useDismiss, useRole, useInteractions, FloatingFocusManager, FloatingList, useHover } from '@floating-ui/react';
8
8
  import clsx from 'clsx';
9
9
  import { flushSync } from 'react-dom';
@@ -75,135 +75,121 @@ 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 } = useCallStateHooks();
84
- const call = useCall();
85
- const mic = useMicrophoneState();
86
- const camera = useCameraState();
87
- const speaker = useSpeakerState();
88
- useEffect(() => {
89
- if (!shouldPersistRef.current)
90
- return;
91
- if (!call)
82
+ const usePersistedDevicePreferences = (key = '@stream-io/device-preferences') => {
83
+ const { useCameraState, useMicrophoneState, useSpeakerState } = 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 } = useCallStateHooks();
90
+ const callingState = useCallCallingState();
91
+ const [applyingState, setApplyingState] = useState('idle');
92
+ const manager = state[deviceKey];
93
+ useEffect(function apply() {
94
+ if (callingState === CallingState.LEFT ||
95
+ !state.devices?.length ||
96
+ applyingState !== 'idle') {
92
97
  return;
93
- if (call.state.callingState === 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
+ useEffect(function persist() {
114
+ if (callingState === 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 = useCall();
133
- const onAppliedRef = useRef(onApplied);
134
- onAppliedRef.current = onApplied;
135
- useEffect(() => {
136
- if (!call)
137
- return;
138
- if (call.state.callingState === 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 = 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
+ return;
186
+ }
187
+ }
205
188
  };
206
- const parseDeviceId = (deviceId) => deviceId !== defaultDevice ? deviceId : undefined;
189
+ const getSelectedDevicePreference = (devices, selectedDevice) => ({
190
+ selectedDeviceId: selectedDevice || defaultDevice,
191
+ selectedDeviceLabel: devices?.find((d) => d.deviceId === selectedDevice)?.label ?? '',
192
+ });
207
193
 
208
194
  const SCROLL_THRESHOLD = 10;
209
195
  /**
@@ -673,7 +659,7 @@ const Video$1 = ({ enabled = true, mirror, trackType, participant, className, Vi
673
659
  }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
674
660
  setVideoElement(element);
675
661
  refs?.setVideoElement?.(element);
676
- } })), isPiP && (jsx(DefaultPictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
662
+ } })), isPiP && PictureInPicturePlaceholder && (jsx(PictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
677
663
  };
678
664
  Video$1.displayName = 'Video';
679
665
 
@@ -838,8 +824,10 @@ const useRenderer = (tfLite) => {
838
824
  output,
839
825
  stop: () => {
840
826
  renderer?.dispose();
841
- videoRef.current && (videoRef.current.srcObject = null);
842
- outputStream && disposeOfMediaStream(outputStream);
827
+ if (videoRef.current)
828
+ videoRef.current.srcObject = null;
829
+ if (outputStream)
830
+ disposeOfMediaStream(outputStream);
843
831
  },
844
832
  };
845
833
  }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
@@ -1033,7 +1021,7 @@ const RecordCallConfirmationButton = ({ caption, }) => {
1033
1021
  }
1034
1022
  const title = isAwaitingResponse
1035
1023
  ? t('Waiting for recording to start...')
1036
- : caption ?? t('Record call');
1024
+ : (caption ?? t('Record call'));
1037
1025
  return (jsx(Restricted, { requiredGrants: [
1038
1026
  OwnCapability.START_RECORD_CALL,
1039
1027
  OwnCapability.STOP_RECORD_CALL,
@@ -1284,7 +1272,7 @@ const DeviceSelectorDropdown = (props) => {
1284
1272
  return (jsxs("div", { className: "str-video__device-settings__device-kind", children: [jsx("div", { className: "str-video__device-settings__device-selector-title", children: title }), jsx(DropDownSelect, { icon: icon, defaultSelectedIndex: selectedIndex, defaultSelectedLabel: selectedDeviceInfo.label, handleSelect: handleSelect, children: deviceList.map((device) => (jsx(DropDownSelectOption, { icon: icon, label: device.label, selected: device.isSelected }, device.deviceId))) })] }));
1285
1273
  };
1286
1274
  const DeviceSelector = (props) => {
1287
- const { visualType = 'list', icon, placeholder, ...rest } = props;
1275
+ const { visualType = 'list', icon, ...rest } = props;
1288
1276
  if (visualType === 'list') {
1289
1277
  return jsx(DeviceSelectorList, { ...rest });
1290
1278
  }
@@ -1339,7 +1327,7 @@ const ToggleAudioPreviewButton = (props) => {
1339
1327
  const handleClick = createCallControlHandler(props, () => microphone.toggle());
1340
1328
  return (jsx(WithTooltip, { title: !hasBrowserPermission
1341
1329
  ? t('Check your browser audio permissions')
1342
- : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1330
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission, "data-testid": optimisticIsMute
1343
1331
  ? 'preview-audio-unmute-button'
1344
1332
  : 'preview-audio-mute-button', onClick: handleClick, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1345
1333
  setTooltipDisabled(shown);
@@ -1365,7 +1353,7 @@ const ToggleAudioPublishingButton = (props) => {
1365
1353
  ? t('You have no permission to share your audio')
1366
1354
  : !hasBrowserPermission
1367
1355
  ? t('Check your browser mic permissions')
1368
- : caption ?? t('Mic'), tooltipDisabled: tooltipDisabled, children: 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) => {
1356
+ : (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: 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
1357
  setTooltipDisabled(shown);
1370
1358
  onMenuToggle?.(shown);
1371
1359
  }, children: [jsx(Icon, { icon: optimisticIsMute ? 'mic-off' : 'mic' }), (!hasBrowserPermission || !hasPermission) && (jsx("span", { className: "str-video__no-media-permission", children: "!" })), isPromptingPermission && (jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }) }) }));
@@ -1380,7 +1368,7 @@ const ToggleVideoPreviewButton = (props) => {
1380
1368
  const handleClick = createCallControlHandler(props, () => camera.toggle());
1381
1369
  return (jsx(WithTooltip, { title: !hasBrowserPermission
1382
1370
  ? t('Check your browser video permissions')
1383
- : caption ?? t('Video'), tooltipDisabled: tooltipDisabled, children: jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1371
+ : (caption ?? t('Video')), tooltipDisabled: tooltipDisabled, children: jsxs(CompositeButton, { active: optimisticIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optimisticIsMute
1384
1372
  ? 'preview-video-unmute-button'
1385
1373
  : 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
1386
1374
  setTooltipDisabled(shown);
@@ -1690,7 +1678,7 @@ const SearchInput = ({ exitSearch, isActive, ...rest }) => {
1690
1678
  }), children: [jsx("input", { placeholder: "Search", ...rest, ref: setInputElement }), isActive ? (jsx("button", { className: "str-video__search-input__clear-btn", onClick: exitSearch, children: jsx("span", { className: "str-video__search-input__icon--active" }) })) : (jsx("span", { className: "str-video__search-input__icon" }))] }));
1691
1679
  };
1692
1680
 
1693
- const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) => {
1681
+ function SearchResults({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) {
1694
1682
  if (searchQueryInProgress) {
1695
1683
  return (jsx("div", { className: "str-video__search-results--loading", children: jsx(LoadingIndicator$1, {}) }));
1696
1684
  }
@@ -1698,7 +1686,7 @@ const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIn
1698
1686
  return jsx(EmptySearchResultComponent, {});
1699
1687
  }
1700
1688
  return jsx(SearchResultList, { data: searchResults });
1701
- };
1689
+ }
1702
1690
 
1703
1691
  const useSearch = ({ debounceInterval, searchFn, searchQuery = '', }) => {
1704
1692
  const [searchResults, setSearchResults] = useState([]);
@@ -2309,7 +2297,7 @@ function checkConditions(obj, conditions) {
2309
2297
  for (const key of Object.keys(conditions)) {
2310
2298
  const operator = conditions[key];
2311
2299
  const maybeOperator = operator && typeof operator === 'object';
2312
- let value = obj[key];
2300
+ const value = obj[key];
2313
2301
  if (maybeOperator && '$eq' in operator) {
2314
2302
  const eqOperator = operator;
2315
2303
  match && (match = eqOperator.$eq === value);
@@ -2680,7 +2668,7 @@ const LivestreamPlayer = (props) => {
2680
2668
  return (jsx(StreamCall, { call: call, children: jsx(LivestreamLayout, { ...layoutProps }) }));
2681
2669
  };
2682
2670
 
2683
- const [major, minor, patch] = ("1.12.10").split('.');
2671
+ const [major, minor, patch] = ("1.13.0").split('.');
2684
2672
  setSdkInfo({
2685
2673
  type: SfuModels.SdkType.REACT,
2686
2674
  major,