@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/CHANGELOG.md +281 -209
- package/dist/index.cjs.js +114 -125
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +115 -126
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/DeviceSettings/DeviceSelector.d.ts +0 -1
- package/dist/src/components/Search/SearchResults.d.ts +1 -1
- package/dist/src/core/hooks/useCalculateHardLimit.d.ts +9 -1
- package/dist/src/hooks/usePersistedDevicePreferences.d.ts +5 -2
- package/package.json +6 -7
- package/src/components/BackgroundFilters/BackgroundFilters.tsx +2 -2
- package/src/components/CallControls/RecordCallButton.tsx +1 -1
- package/src/components/CallControls/ToggleAudioButton.tsx +3 -3
- package/src/components/CallControls/ToggleVideoButton.tsx +2 -2
- package/src/components/DeviceSettings/DeviceSelector.tsx +2 -3
- package/src/components/Search/SearchResults.tsx +3 -3
- package/src/core/components/Video/Video.tsx +2 -2
- package/src/hooks/usePersistedDevicePreferences.ts +183 -124
- package/src/utilities/filter.ts +1 -1
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,
|
|
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,122 @@ const useFloatingUIPreset = ({ middleware = [], placement, strategy, offset: off
|
|
|
75
75
|
|
|
76
76
|
const defaultDevice = 'default';
|
|
77
77
|
/**
|
|
78
|
-
* This hook will persist the device
|
|
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
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
127
|
+
console.warn(`Failed to save ${deviceKey} device preferences`, err);
|
|
114
128
|
}
|
|
115
129
|
}, [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
applyingState,
|
|
131
|
+
callingState,
|
|
132
|
+
deviceKey,
|
|
119
133
|
key,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
shouldPersistRef,
|
|
134
|
+
state.devices,
|
|
135
|
+
state.isMute,
|
|
136
|
+
state.selectedDevice,
|
|
124
137
|
]);
|
|
125
138
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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 && (jsx(
|
|
663
|
+
} })), isPiP && PictureInPicturePlaceholder && (jsx(PictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (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
|
-
|
|
842
|
-
|
|
828
|
+
if (videoRef.current)
|
|
829
|
+
videoRef.current.srcObject = null;
|
|
830
|
+
if (outputStream)
|
|
831
|
+
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 (jsx(Restricted, { requiredGrants: [
|
|
1038
1027
|
OwnCapability.START_RECORD_CALL,
|
|
1039
1028
|
OwnCapability.STOP_RECORD_CALL,
|
|
@@ -1284,7 +1273,7 @@ const DeviceSelectorDropdown = (props) => {
|
|
|
1284
1273
|
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
1274
|
};
|
|
1286
1275
|
const DeviceSelector = (props) => {
|
|
1287
|
-
const { visualType = 'list', icon,
|
|
1276
|
+
const { visualType = 'list', icon, ...rest } = props;
|
|
1288
1277
|
if (visualType === 'list') {
|
|
1289
1278
|
return jsx(DeviceSelectorList, { ...rest });
|
|
1290
1279
|
}
|
|
@@ -1339,7 +1328,7 @@ const ToggleAudioPreviewButton = (props) => {
|
|
|
1339
1328
|
const handleClick = createCallControlHandler(props, () => microphone.toggle());
|
|
1340
1329
|
return (jsx(WithTooltip, { title: !hasBrowserPermission
|
|
1341
1330
|
? 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
|
|
1331
|
+
: (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
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: 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: 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: [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 +1369,7 @@ const ToggleVideoPreviewButton = (props) => {
|
|
|
1380
1369
|
const handleClick = createCallControlHandler(props, () => camera.toggle());
|
|
1381
1370
|
return (jsx(WithTooltip, { title: !hasBrowserPermission
|
|
1382
1371
|
? 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
|
|
1372
|
+
: (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
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: [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
1680
|
};
|
|
1692
1681
|
|
|
1693
|
-
|
|
1682
|
+
function SearchResults({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) {
|
|
1694
1683
|
if (searchQueryInProgress) {
|
|
1695
1684
|
return (jsx("div", { className: "str-video__search-results--loading", children: jsx(LoadingIndicator$1, {}) }));
|
|
1696
1685
|
}
|
|
@@ -1698,7 +1687,7 @@ const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIn
|
|
|
1698
1687
|
return jsx(EmptySearchResultComponent, {});
|
|
1699
1688
|
}
|
|
1700
1689
|
return jsx(SearchResultList, { data: searchResults });
|
|
1701
|
-
}
|
|
1690
|
+
}
|
|
1702
1691
|
|
|
1703
1692
|
const useSearch = ({ debounceInterval, searchFn, searchQuery = '', }) => {
|
|
1704
1693
|
const [searchResults, setSearchResults] = 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
|
-
|
|
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 (jsx(StreamCall, { call: call, children: jsx(LivestreamLayout, { ...layoutProps }) }));
|
|
2681
2670
|
};
|
|
2682
2671
|
|
|
2683
|
-
const [major, minor, patch] = ("1.
|
|
2672
|
+
const [major, minor, patch] = ("1.13.1").split('.');
|
|
2684
2673
|
setSdkInfo({
|
|
2685
2674
|
type: SfuModels.SdkType.REACT,
|
|
2686
2675
|
major,
|