@stream-io/video-react-sdk 1.32.4 → 1.33.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/CHANGELOG.md +13 -0
- package/dist/css/embedded.css +3630 -0
- package/dist/css/embedded.css.map +1 -0
- package/dist/css/styles.css +13 -2
- package/dist/css/styles.css.map +1 -1
- package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js +353 -0
- package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js.map +1 -0
- package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js +355 -0
- package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js.map +1 -0
- package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js +57 -0
- package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js.map +1 -0
- package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js +59 -0
- package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js.map +1 -0
- package/dist/embedded.cjs.js +3410 -0
- package/dist/embedded.cjs.js.map +1 -0
- package/dist/embedded.d.ts +1 -0
- package/dist/embedded.es.js +3407 -0
- package/dist/embedded.es.js.map +1 -0
- package/dist/index.cjs.js +67 -202
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +69 -204
- package/dist/index.es.js.map +1 -1
- package/dist/src/embedded/EmbeddedClientProvider.d.ts +21 -0
- package/dist/src/embedded/call/CallControls.d.ts +9 -0
- package/dist/src/embedded/call/CallHeader.d.ts +4 -0
- package/dist/src/embedded/call/CallLayout.d.ts +4 -0
- package/dist/src/embedded/call/CallStateRouter.d.ts +4 -0
- package/dist/src/embedded/call/EmbeddedCall.d.ts +6 -0
- package/dist/src/embedded/call/index.d.ts +1 -0
- package/dist/src/embedded/context/ConfigurationContext.d.ts +11 -0
- package/dist/src/embedded/context/index.d.ts +1 -0
- package/dist/src/embedded/hooks/index.d.ts +8 -0
- package/dist/src/embedded/hooks/useCallDuration.d.ts +7 -0
- package/dist/src/embedded/hooks/useEmbeddedClient.d.ts +22 -0
- package/dist/src/embedded/hooks/useInitializeCall.d.ts +11 -0
- package/dist/src/embedded/hooks/useInitializeVideoClient.d.ts +16 -0
- package/dist/src/embedded/hooks/useIsLivestreamPaused.d.ts +8 -0
- package/dist/src/embedded/hooks/useLayout.d.ts +9 -0
- package/dist/src/embedded/hooks/useNoiseCancellationLoader.d.ts +12 -0
- package/dist/src/embedded/hooks/useWakeLock.d.ts +5 -0
- package/dist/src/embedded/index.d.ts +3 -0
- package/dist/src/embedded/livestream/EmbeddedLivestream.d.ts +5 -0
- package/dist/src/embedded/livestream/LivestreamUI.d.ts +1 -0
- package/dist/src/embedded/livestream/host/HostLayout.d.ts +7 -0
- package/dist/src/embedded/livestream/host/HostStateRouter.d.ts +1 -0
- package/dist/src/embedded/livestream/index.d.ts +1 -0
- package/dist/src/embedded/livestream/viewer/ViewerLayout.d.ts +1 -0
- package/dist/src/embedded/livestream/viewer/ViewerLobby.d.ts +4 -0
- package/dist/src/embedded/livestream/viewer/ViewerStateRouter.d.ts +1 -0
- package/dist/src/embedded/shared/BlurToggleButton/BlurToggleButton.d.ts +2 -0
- package/dist/src/embedded/shared/CallFeedback/CallEndedScreen.d.ts +6 -0
- package/dist/src/embedded/shared/CallFeedback/CallFeedback.d.ts +4 -0
- package/dist/src/embedded/shared/CallFeedback/RatingScreen.d.ts +5 -0
- package/dist/src/embedded/shared/CallFeedback/StarRating.d.ts +6 -0
- package/dist/src/embedded/shared/CallFeedback/ThankYouScreen.d.ts +1 -0
- package/dist/src/embedded/shared/ConnectionNotification/ConnectionNotification.d.ts +1 -0
- package/dist/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.d.ts +1 -0
- package/dist/src/embedded/shared/JoinError/JoinError.d.ts +5 -0
- package/dist/src/embedded/shared/Lobby/DeviceControls.d.ts +5 -0
- package/dist/src/embedded/shared/Lobby/DisabledDeviceButton.d.ts +6 -0
- package/dist/src/embedded/shared/Lobby/Lobby.d.ts +10 -0
- package/dist/src/embedded/shared/Lobby/ToggleCameraButton.d.ts +1 -0
- package/dist/src/embedded/shared/Lobby/ToggleMicButton.d.ts +1 -0
- package/dist/src/embedded/shared/Lobby/VideoPreviewFallbacks.d.ts +2 -0
- package/dist/src/embedded/shared/ViewersCount/ViewersCount.d.ts +5 -0
- package/dist/src/embedded/shared/index.d.ts +7 -0
- package/dist/src/embedded/types.d.ts +65 -0
- package/dist/src/hooks/usePersistedDevicePreferences.d.ts +3 -12
- package/dist/src/translations/index.d.ts +42 -1
- package/embedded.ts +1 -0
- package/package.json +18 -4
- package/src/core/components/CallLayout/LivestreamLayout.tsx +53 -41
- package/src/embedded/EmbeddedClientProvider.tsx +125 -0
- package/src/embedded/call/CallControls.tsx +124 -0
- package/src/embedded/call/CallHeader.tsx +30 -0
- package/src/embedded/call/CallLayout.tsx +66 -0
- package/src/embedded/call/CallStateRouter.tsx +56 -0
- package/src/embedded/call/EmbeddedCall.tsx +14 -0
- package/src/embedded/call/index.ts +1 -0
- package/src/embedded/context/ConfigurationContext.tsx +36 -0
- package/src/embedded/context/index.ts +1 -0
- package/src/embedded/hooks/index.ts +8 -0
- package/src/embedded/hooks/useCallDuration.ts +40 -0
- package/src/embedded/hooks/useEmbeddedClient.ts +64 -0
- package/src/embedded/hooks/useInitializeCall.ts +51 -0
- package/src/embedded/hooks/useInitializeVideoClient.ts +118 -0
- package/src/embedded/hooks/useIsLivestreamPaused.ts +44 -0
- package/src/embedded/hooks/useLayout.ts +100 -0
- package/src/embedded/hooks/useNoiseCancellationLoader.ts +62 -0
- package/src/embedded/hooks/useWakeLock.ts +33 -0
- package/src/embedded/index.ts +12 -0
- package/src/embedded/livestream/EmbeddedLivestream.tsx +16 -0
- package/src/embedded/livestream/LivestreamUI.tsx +17 -0
- package/src/embedded/livestream/host/HostLayout.tsx +210 -0
- package/src/embedded/livestream/host/HostStateRouter.tsx +100 -0
- package/src/embedded/livestream/index.ts +1 -0
- package/src/embedded/livestream/viewer/ViewerLayout.tsx +160 -0
- package/src/embedded/livestream/viewer/ViewerLobby.tsx +135 -0
- package/src/embedded/livestream/viewer/ViewerStateRouter.tsx +82 -0
- package/src/embedded/shared/BlurToggleButton/BlurToggleButton.tsx +75 -0
- package/src/embedded/shared/CallFeedback/CallEndedScreen.tsx +55 -0
- package/src/embedded/shared/CallFeedback/CallFeedback.tsx +51 -0
- package/src/embedded/shared/CallFeedback/RatingScreen.tsx +47 -0
- package/src/embedded/shared/CallFeedback/StarRating.tsx +46 -0
- package/src/embedded/shared/CallFeedback/ThankYouScreen.tsx +19 -0
- package/src/embedded/shared/ConnectionNotification/ConnectionNotification.tsx +59 -0
- package/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.tsx +32 -0
- package/src/embedded/shared/JoinError/JoinError.tsx +27 -0
- package/src/embedded/shared/Lobby/DeviceControls.tsx +54 -0
- package/src/embedded/shared/Lobby/DisabledDeviceButton.tsx +21 -0
- package/src/embedded/shared/Lobby/Lobby.tsx +59 -0
- package/src/embedded/shared/Lobby/ToggleCameraButton.tsx +44 -0
- package/src/embedded/shared/Lobby/ToggleMicButton.tsx +48 -0
- package/src/embedded/shared/Lobby/VideoPreviewFallbacks.tsx +55 -0
- package/src/embedded/shared/ViewersCount/ViewersCount.tsx +18 -0
- package/src/embedded/shared/index.ts +7 -0
- package/src/embedded/types.ts +80 -0
- package/src/hooks/usePersistedDevicePreferences.ts +8 -307
- package/src/translations/en.json +44 -2
|
@@ -0,0 +1,3410 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var videoReactBindings = require('@stream-io/video-react-bindings');
|
|
6
|
+
var videoClient = require('@stream-io/video-client');
|
|
7
|
+
var react$1 = require('@floating-ui/react');
|
|
8
|
+
var clsx = require('clsx');
|
|
9
|
+
|
|
10
|
+
const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
|
|
11
|
+
const call = videoReactBindings.useCall();
|
|
12
|
+
const [audioElement, setAudioElement] = react.useState(null);
|
|
13
|
+
const { userId, sessionId } = participant;
|
|
14
|
+
react.useEffect(() => {
|
|
15
|
+
if (!call || !audioElement)
|
|
16
|
+
return;
|
|
17
|
+
const cleanup = call.bindAudioElement(audioElement, sessionId, trackType);
|
|
18
|
+
return () => {
|
|
19
|
+
cleanup?.();
|
|
20
|
+
};
|
|
21
|
+
}, [call, sessionId, audioElement, trackType]);
|
|
22
|
+
return (jsxRuntime.jsx("audio", { autoPlay: true, ...rest, ref: setAudioElement, "data-user-id": userId, "data-session-id": sessionId, "data-track-type": trackType }));
|
|
23
|
+
};
|
|
24
|
+
Audio.displayName = 'Audio';
|
|
25
|
+
|
|
26
|
+
const ParticipantsAudio = (props) => {
|
|
27
|
+
const { participants, audioProps } = props;
|
|
28
|
+
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: participants.map((participant) => {
|
|
29
|
+
if (participant.isLocalParticipant)
|
|
30
|
+
return null;
|
|
31
|
+
const { audioStream, screenShareAudioStream, sessionId } = participant;
|
|
32
|
+
const hasAudioTrack = videoClient.hasAudio(participant);
|
|
33
|
+
const audioTrackElement = hasAudioTrack && audioStream && (jsxRuntime.jsx(Audio, { ...audioProps, trackType: "audioTrack", participant: participant }));
|
|
34
|
+
const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
|
|
35
|
+
const screenShareAudioTrackElement = hasScreenShareAudioTrack &&
|
|
36
|
+
screenShareAudioStream && (jsxRuntime.jsx(Audio, { ...audioProps, trackType: "screenShareAudioTrack", participant: participant }));
|
|
37
|
+
return (jsxRuntime.jsxs(react.Fragment, { children: [audioTrackElement, screenShareAudioTrackElement] }, sessionId));
|
|
38
|
+
}) }));
|
|
39
|
+
};
|
|
40
|
+
ParticipantsAudio.displayName = 'ParticipantsAudio';
|
|
41
|
+
|
|
42
|
+
const ParticipantViewContext = react.createContext(undefined);
|
|
43
|
+
const useParticipantViewContext = () => react.useContext(ParticipantViewContext);
|
|
44
|
+
|
|
45
|
+
const useFloatingUIPreset = ({ middleware = [], placement, strategy, offset: offsetInPx = 10, }) => {
|
|
46
|
+
const { refs, x, y, update, elements: { domReference, floating }, context, } = react$1.useFloating({
|
|
47
|
+
placement,
|
|
48
|
+
strategy,
|
|
49
|
+
middleware: [
|
|
50
|
+
react$1.offset(offsetInPx),
|
|
51
|
+
react$1.shift(),
|
|
52
|
+
react$1.flip(),
|
|
53
|
+
react$1.size({
|
|
54
|
+
padding: 10,
|
|
55
|
+
apply: ({ availableHeight, elements }) => {
|
|
56
|
+
Object.assign(elements.floating.style, {
|
|
57
|
+
maxHeight: `${availableHeight}px`,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
...middleware,
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
// handle window resizing
|
|
65
|
+
react.useEffect(() => {
|
|
66
|
+
if (!domReference || !floating)
|
|
67
|
+
return;
|
|
68
|
+
const cleanup = react$1.autoUpdate(domReference, floating, update);
|
|
69
|
+
return () => cleanup();
|
|
70
|
+
}, [domReference, floating, update]);
|
|
71
|
+
return { refs, x, y, domReference, floating, strategy, context };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const SCROLL_THRESHOLD = 10;
|
|
75
|
+
/**
|
|
76
|
+
* Hook which observes element's scroll position and returns text value based on the
|
|
77
|
+
* position of the scrollbar (`top`, `bottom`, `between` and `null` if no scrollbar is available)
|
|
78
|
+
*/
|
|
79
|
+
const useVerticalScrollPosition = (scrollElement, threshold = SCROLL_THRESHOLD) => {
|
|
80
|
+
const [scrollPosition, setScrollPosition] = react.useState(null);
|
|
81
|
+
react.useEffect(() => {
|
|
82
|
+
if (!scrollElement)
|
|
83
|
+
return;
|
|
84
|
+
const scrollHandler = () => {
|
|
85
|
+
const element = scrollElement;
|
|
86
|
+
const hasVerticalScrollbar = element.scrollHeight > element.clientHeight;
|
|
87
|
+
if (!hasVerticalScrollbar)
|
|
88
|
+
return setScrollPosition(null);
|
|
89
|
+
const isAtTheTop = element.scrollTop <= threshold;
|
|
90
|
+
if (isAtTheTop)
|
|
91
|
+
return setScrollPosition('top');
|
|
92
|
+
const isAtTheBottom = Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight) <= threshold;
|
|
93
|
+
if (isAtTheBottom)
|
|
94
|
+
return setScrollPosition('bottom');
|
|
95
|
+
setScrollPosition('between');
|
|
96
|
+
};
|
|
97
|
+
const resizeObserver = new ResizeObserver(scrollHandler);
|
|
98
|
+
resizeObserver.observe(scrollElement);
|
|
99
|
+
scrollElement.addEventListener('scroll', scrollHandler);
|
|
100
|
+
return () => {
|
|
101
|
+
scrollElement.removeEventListener('scroll', scrollHandler);
|
|
102
|
+
resizeObserver.disconnect();
|
|
103
|
+
};
|
|
104
|
+
}, [scrollElement, threshold]);
|
|
105
|
+
return scrollPosition;
|
|
106
|
+
};
|
|
107
|
+
const useHorizontalScrollPosition = (scrollElement, threshold = SCROLL_THRESHOLD) => {
|
|
108
|
+
const [scrollPosition, setScrollPosition] = react.useState(null);
|
|
109
|
+
react.useEffect(() => {
|
|
110
|
+
if (!scrollElement)
|
|
111
|
+
return;
|
|
112
|
+
const scrollHandler = () => {
|
|
113
|
+
const element = scrollElement;
|
|
114
|
+
const hasHorizontalScrollbar = element.scrollWidth > element.clientWidth;
|
|
115
|
+
if (!hasHorizontalScrollbar)
|
|
116
|
+
return setScrollPosition(null);
|
|
117
|
+
const isAtTheStart = element.scrollLeft <= threshold;
|
|
118
|
+
if (isAtTheStart)
|
|
119
|
+
return setScrollPosition('start');
|
|
120
|
+
const isAtTheEnd = Math.abs(element.scrollWidth - element.scrollLeft - element.clientWidth) <= threshold;
|
|
121
|
+
if (isAtTheEnd)
|
|
122
|
+
return setScrollPosition('end');
|
|
123
|
+
setScrollPosition('between');
|
|
124
|
+
};
|
|
125
|
+
const resizeObserver = new ResizeObserver(scrollHandler);
|
|
126
|
+
resizeObserver.observe(scrollElement);
|
|
127
|
+
scrollElement.addEventListener('scroll', scrollHandler);
|
|
128
|
+
return () => {
|
|
129
|
+
scrollElement.removeEventListener('scroll', scrollHandler);
|
|
130
|
+
resizeObserver.disconnect();
|
|
131
|
+
};
|
|
132
|
+
}, [scrollElement, threshold]);
|
|
133
|
+
return scrollPosition;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const useRequestPermission = (permission) => {
|
|
137
|
+
const call = videoReactBindings.useCall();
|
|
138
|
+
const { useHasPermissions } = videoReactBindings.useCallStateHooks();
|
|
139
|
+
const hasPermission = useHasPermissions(permission);
|
|
140
|
+
const [isAwaitingPermission, setIsAwaitingPermission] = react.useState(false); // TODO: load with possibly pending state
|
|
141
|
+
react.useEffect(() => {
|
|
142
|
+
const reset = () => setIsAwaitingPermission(false);
|
|
143
|
+
if (hasPermission)
|
|
144
|
+
reset();
|
|
145
|
+
}, [hasPermission]);
|
|
146
|
+
const requestPermission = react.useCallback(async () => {
|
|
147
|
+
if (hasPermission)
|
|
148
|
+
return true;
|
|
149
|
+
const canRequestPermission = !!call?.permissionsContext.canRequest(permission);
|
|
150
|
+
if (isAwaitingPermission || !canRequestPermission)
|
|
151
|
+
return false;
|
|
152
|
+
setIsAwaitingPermission(true);
|
|
153
|
+
try {
|
|
154
|
+
await call?.requestPermissions({
|
|
155
|
+
permissions: [permission],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
setIsAwaitingPermission(false);
|
|
160
|
+
throw new Error(`requestPermission failed: ${error}`);
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}, [call, hasPermission, isAwaitingPermission, permission]);
|
|
164
|
+
return {
|
|
165
|
+
requestPermission,
|
|
166
|
+
hasPermission,
|
|
167
|
+
canRequestPermission: !!call?.permissionsContext.canRequest(permission),
|
|
168
|
+
isAwaitingPermission,
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Utility hook that helps render a list of devices or implement a device selector.
|
|
174
|
+
* Compared to someting like `useCameraState().devices`, it has some handy features:
|
|
175
|
+
* 1. Adds the "Default" device to the list if applicable (either the user did not
|
|
176
|
+
* select a device, or a previously selected device is no longer available).
|
|
177
|
+
* 2. Maps the device list to a format more suitable for rendering.
|
|
178
|
+
*/
|
|
179
|
+
function useDeviceList(devices, selectedDeviceId) {
|
|
180
|
+
const { t } = videoReactBindings.useI18n();
|
|
181
|
+
return react.useMemo(() => {
|
|
182
|
+
let selectedDeviceInfo = null;
|
|
183
|
+
let selectedIndex = null;
|
|
184
|
+
const deviceList = devices.map((d, i) => {
|
|
185
|
+
const isSelected = d.deviceId === selectedDeviceId;
|
|
186
|
+
const device = { deviceId: d.deviceId, label: d.label, isSelected };
|
|
187
|
+
if (isSelected) {
|
|
188
|
+
selectedDeviceInfo = device;
|
|
189
|
+
selectedIndex = i;
|
|
190
|
+
}
|
|
191
|
+
return device;
|
|
192
|
+
});
|
|
193
|
+
if (selectedDeviceInfo === null || selectedIndex === null) {
|
|
194
|
+
const defaultDevice = {
|
|
195
|
+
deviceId: 'default',
|
|
196
|
+
label: t('Default'),
|
|
197
|
+
isSelected: true,
|
|
198
|
+
};
|
|
199
|
+
selectedDeviceInfo = defaultDevice;
|
|
200
|
+
selectedIndex = 0;
|
|
201
|
+
deviceList.unshift(defaultDevice);
|
|
202
|
+
}
|
|
203
|
+
return { deviceList, selectedDeviceInfo, selectedIndex };
|
|
204
|
+
}, [devices, selectedDeviceId, t]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Enables drag-to-scroll functionality with momentum scrolling on a scrollable element.
|
|
209
|
+
*
|
|
210
|
+
* This hook allows users to click and drag to scroll an element, with momentum scrolling
|
|
211
|
+
* that continues after the drag ends. The drag only activates after moving beyond a threshold
|
|
212
|
+
* distance, which prevents accidental drags from clicks.
|
|
213
|
+
*
|
|
214
|
+
* @param element - The HTML element to enable drag to scroll on.
|
|
215
|
+
* @param options - Options for customizing the drag-to-scroll behavior.
|
|
216
|
+
*/
|
|
217
|
+
function useDragToScroll(element, options = {}) {
|
|
218
|
+
const stateRef = react.useRef({
|
|
219
|
+
isDragging: false,
|
|
220
|
+
isPointerActive: false,
|
|
221
|
+
prevX: 0,
|
|
222
|
+
prevY: 0,
|
|
223
|
+
velocityX: 0,
|
|
224
|
+
velocityY: 0,
|
|
225
|
+
rafId: 0,
|
|
226
|
+
startX: 0,
|
|
227
|
+
startY: 0,
|
|
228
|
+
});
|
|
229
|
+
react.useEffect(() => {
|
|
230
|
+
if (!element || !options.enabled)
|
|
231
|
+
return;
|
|
232
|
+
const { decay = 0.95, minVelocity = 0.5, dragThreshold = 5 } = options;
|
|
233
|
+
const state = stateRef.current;
|
|
234
|
+
const stopMomentum = () => {
|
|
235
|
+
if (state.rafId) {
|
|
236
|
+
cancelAnimationFrame(state.rafId);
|
|
237
|
+
state.rafId = 0;
|
|
238
|
+
}
|
|
239
|
+
state.velocityX = 0;
|
|
240
|
+
state.velocityY = 0;
|
|
241
|
+
};
|
|
242
|
+
const momentumStep = () => {
|
|
243
|
+
state.velocityX *= decay;
|
|
244
|
+
state.velocityY *= decay;
|
|
245
|
+
element.scrollLeft -= state.velocityX;
|
|
246
|
+
element.scrollTop -= state.velocityY;
|
|
247
|
+
if (Math.abs(state.velocityX) < minVelocity &&
|
|
248
|
+
Math.abs(state.velocityY) < minVelocity) {
|
|
249
|
+
state.rafId = 0;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.rafId = requestAnimationFrame(momentumStep);
|
|
253
|
+
};
|
|
254
|
+
const onPointerDown = (e) => {
|
|
255
|
+
if (e.pointerType !== 'mouse')
|
|
256
|
+
return;
|
|
257
|
+
stopMomentum();
|
|
258
|
+
state.isDragging = false;
|
|
259
|
+
state.isPointerActive = true;
|
|
260
|
+
state.prevX = e.clientX;
|
|
261
|
+
state.prevY = e.clientY;
|
|
262
|
+
state.startX = e.clientX;
|
|
263
|
+
state.startY = e.clientY;
|
|
264
|
+
};
|
|
265
|
+
const onPointerMove = (e) => {
|
|
266
|
+
if (e.pointerType !== 'mouse')
|
|
267
|
+
return;
|
|
268
|
+
if (!state.isPointerActive)
|
|
269
|
+
return;
|
|
270
|
+
const dx = e.clientX - state.startX;
|
|
271
|
+
const dy = e.clientY - state.startY;
|
|
272
|
+
if (!state.isDragging && Math.hypot(dx, dy) > dragThreshold) {
|
|
273
|
+
state.isDragging = true;
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
}
|
|
276
|
+
if (!state.isDragging)
|
|
277
|
+
return;
|
|
278
|
+
const moveDx = e.clientX - state.prevX;
|
|
279
|
+
const moveDy = e.clientY - state.prevY;
|
|
280
|
+
element.scrollLeft -= moveDx;
|
|
281
|
+
element.scrollTop -= moveDy;
|
|
282
|
+
state.velocityX = moveDx;
|
|
283
|
+
state.velocityY = moveDy;
|
|
284
|
+
state.prevX = e.clientX;
|
|
285
|
+
state.prevY = e.clientY;
|
|
286
|
+
};
|
|
287
|
+
const onPointerUpOrCancel = () => {
|
|
288
|
+
const wasDragging = state.isDragging;
|
|
289
|
+
state.isDragging = false;
|
|
290
|
+
state.isPointerActive = false;
|
|
291
|
+
state.prevX = 0;
|
|
292
|
+
state.prevY = 0;
|
|
293
|
+
state.startX = 0;
|
|
294
|
+
state.startY = 0;
|
|
295
|
+
if (!wasDragging) {
|
|
296
|
+
stopMomentum();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (Math.hypot(state.velocityX, state.velocityY) < minVelocity) {
|
|
300
|
+
stopMomentum();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
state.rafId = requestAnimationFrame(momentumStep);
|
|
304
|
+
};
|
|
305
|
+
element.addEventListener('pointerdown', onPointerDown);
|
|
306
|
+
element.addEventListener('pointermove', onPointerMove);
|
|
307
|
+
window.addEventListener('pointerup', onPointerUpOrCancel);
|
|
308
|
+
window.addEventListener('pointercancel', onPointerUpOrCancel);
|
|
309
|
+
return () => {
|
|
310
|
+
element.removeEventListener('pointerdown', onPointerDown);
|
|
311
|
+
element.removeEventListener('pointermove', onPointerMove);
|
|
312
|
+
window.removeEventListener('pointerup', onPointerUpOrCancel);
|
|
313
|
+
window.removeEventListener('pointercancel', onPointerUpOrCancel);
|
|
314
|
+
stopMomentum();
|
|
315
|
+
};
|
|
316
|
+
}, [element, options]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var MenuVisualType;
|
|
320
|
+
(function (MenuVisualType) {
|
|
321
|
+
MenuVisualType["PORTAL"] = "portal";
|
|
322
|
+
MenuVisualType["MENU"] = "menu";
|
|
323
|
+
})(MenuVisualType || (MenuVisualType = {}));
|
|
324
|
+
/**
|
|
325
|
+
* Used to provide utility APIs to the components rendered inside the portal.
|
|
326
|
+
*/
|
|
327
|
+
const MenuContext = react.createContext({});
|
|
328
|
+
/**
|
|
329
|
+
* Access to the closes MenuContext.
|
|
330
|
+
*/
|
|
331
|
+
const useMenuContext = () => {
|
|
332
|
+
return react.useContext(MenuContext);
|
|
333
|
+
};
|
|
334
|
+
const MenuPortal = ({ children, refs, }) => {
|
|
335
|
+
const [portalRoot, setPortalRoot] = react.useState(null);
|
|
336
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { ref: setPortalRoot, className: "str-video__portal" }), jsxRuntime.jsx(react$1.FloatingOverlay, { children: jsxRuntime.jsx(react$1.FloatingPortal, { root: portalRoot, children: jsxRuntime.jsx("div", { className: "str-video__portal-content", ref: refs.setFloating, children: children }) }) })] }));
|
|
337
|
+
};
|
|
338
|
+
const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', offset, visualType = MenuVisualType.MENU, children, onToggle, }) => {
|
|
339
|
+
const [menuShown, setMenuShown] = react.useState(false);
|
|
340
|
+
const toggleHandler = react.useRef(onToggle);
|
|
341
|
+
toggleHandler.current = onToggle;
|
|
342
|
+
const { floating, domReference, refs, x, y } = useFloatingUIPreset({
|
|
343
|
+
placement,
|
|
344
|
+
strategy,
|
|
345
|
+
offset,
|
|
346
|
+
});
|
|
347
|
+
react.useEffect(() => {
|
|
348
|
+
const parentDocument = domReference?.ownerDocument;
|
|
349
|
+
const handleClick = (event) => {
|
|
350
|
+
if (!floating && domReference?.contains(event.target)) {
|
|
351
|
+
setMenuShown(true);
|
|
352
|
+
toggleHandler.current?.(true);
|
|
353
|
+
}
|
|
354
|
+
else if (floating && !floating?.contains(event.target)) {
|
|
355
|
+
setMenuShown(false);
|
|
356
|
+
toggleHandler.current?.(false);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const handleKeyDown = (event) => {
|
|
360
|
+
if (event.key && // key can be undefined in some browsers
|
|
361
|
+
event.key.toLowerCase() === 'escape' &&
|
|
362
|
+
!event.altKey &&
|
|
363
|
+
!event.ctrlKey) {
|
|
364
|
+
setMenuShown(false);
|
|
365
|
+
toggleHandler.current?.(false);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
parentDocument?.addEventListener('click', handleClick, { capture: true });
|
|
369
|
+
parentDocument?.addEventListener('keydown', handleKeyDown);
|
|
370
|
+
return () => {
|
|
371
|
+
parentDocument?.removeEventListener('click', handleClick, {
|
|
372
|
+
capture: true,
|
|
373
|
+
});
|
|
374
|
+
parentDocument?.removeEventListener('keydown', handleKeyDown);
|
|
375
|
+
};
|
|
376
|
+
}, [floating, domReference]);
|
|
377
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [menuShown && (jsxRuntime.jsx(MenuContext.Provider, { value: { close: () => setMenuShown(false) }, children: visualType === MenuVisualType.PORTAL ? (jsxRuntime.jsx(MenuPortal, { refs: refs, children: children })) : visualType === MenuVisualType.MENU ? (jsxRuntime.jsx("div", { className: "str-video__menu-container", ref: refs.setFloating, style: {
|
|
378
|
+
position: strategy,
|
|
379
|
+
top: y ?? 0,
|
|
380
|
+
left: x ?? 0,
|
|
381
|
+
overflowY: 'auto',
|
|
382
|
+
}, role: "menu", children: children })) : null })), jsxRuntime.jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const GenericMenu = ({ children, onItemClick, }) => {
|
|
386
|
+
const ref = react.useRef(null);
|
|
387
|
+
return (jsxRuntime.jsx("ul", { className: "str-video__generic-menu", ref: ref, onClick: (e) => {
|
|
388
|
+
if (onItemClick &&
|
|
389
|
+
e.target !== ref.current &&
|
|
390
|
+
ref.current?.contains(e.target)) {
|
|
391
|
+
onItemClick(e);
|
|
392
|
+
}
|
|
393
|
+
}, children: children }));
|
|
394
|
+
};
|
|
395
|
+
const GenericMenuButtonItem = ({ children, ...rest }) => {
|
|
396
|
+
return (jsxRuntime.jsx("li", { className: "str-video__generic-menu--item", children: jsxRuntime.jsx("button", { ...rest, children: children }) }));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const Icon = ({ className, icon }) => (jsxRuntime.jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`, className) }));
|
|
400
|
+
|
|
401
|
+
function usePictureInPictureState(videoElement) {
|
|
402
|
+
const [isPiP, setIsPiP] = react.useState(document.pictureInPictureElement === videoElement);
|
|
403
|
+
if (!videoElement && isPiP)
|
|
404
|
+
setIsPiP(false);
|
|
405
|
+
react.useEffect(() => {
|
|
406
|
+
if (!videoElement)
|
|
407
|
+
return;
|
|
408
|
+
const handlePiP = () => {
|
|
409
|
+
setIsPiP(document.pictureInPictureElement === videoElement);
|
|
410
|
+
};
|
|
411
|
+
videoElement.addEventListener('enterpictureinpicture', handlePiP);
|
|
412
|
+
videoElement.addEventListener('leavepictureinpicture', handlePiP);
|
|
413
|
+
return () => {
|
|
414
|
+
videoElement.removeEventListener('enterpictureinpicture', handlePiP);
|
|
415
|
+
videoElement.removeEventListener('leavepictureinpicture', handlePiP);
|
|
416
|
+
};
|
|
417
|
+
}, [videoElement]);
|
|
418
|
+
return isPiP;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const ParticipantActionsContextMenu = () => {
|
|
422
|
+
const { participant, participantViewElement, videoElement } = useParticipantViewContext();
|
|
423
|
+
const [fullscreenModeOn, setFullscreenModeOn] = react.useState(!!document.fullscreenElement);
|
|
424
|
+
const call = videoReactBindings.useCall();
|
|
425
|
+
const isPiP = usePictureInPictureState(videoElement ?? undefined);
|
|
426
|
+
const { t } = videoReactBindings.useI18n();
|
|
427
|
+
const { pin, sessionId, userId } = participant;
|
|
428
|
+
const hasAudioTrack = videoClient.hasAudio(participant);
|
|
429
|
+
const hasVideoTrack = videoClient.hasVideo(participant);
|
|
430
|
+
const hasScreenShareTrack = videoClient.hasScreenShare(participant);
|
|
431
|
+
const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
|
|
432
|
+
const blockUser = () => call?.blockUser(userId);
|
|
433
|
+
const kickUser = () => call?.kickUser({ user_id: userId });
|
|
434
|
+
const muteAudio = () => call?.muteUser(userId, 'audio');
|
|
435
|
+
const muteVideo = () => call?.muteUser(userId, 'video');
|
|
436
|
+
const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
|
|
437
|
+
const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
|
|
438
|
+
const grantPermission = (permission) => () => {
|
|
439
|
+
call?.updateUserPermissions({
|
|
440
|
+
user_id: userId,
|
|
441
|
+
grant_permissions: [permission],
|
|
442
|
+
});
|
|
443
|
+
};
|
|
444
|
+
const revokePermission = (permission) => () => {
|
|
445
|
+
call?.updateUserPermissions({
|
|
446
|
+
user_id: userId,
|
|
447
|
+
revoke_permissions: [permission],
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
const toggleParticipantPin = () => {
|
|
451
|
+
if (pin) {
|
|
452
|
+
call?.unpin(sessionId);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
call?.pin(sessionId);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const pinForEveryone = () => {
|
|
459
|
+
call
|
|
460
|
+
?.pinForEveryone({ user_id: userId, session_id: sessionId })
|
|
461
|
+
.catch((err) => {
|
|
462
|
+
console.error(`Failed to pin participant ${userId}`, err);
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
const unpinForEveryone = () => {
|
|
466
|
+
call
|
|
467
|
+
?.unpinForEveryone({ user_id: userId, session_id: sessionId })
|
|
468
|
+
.catch((err) => {
|
|
469
|
+
console.error(`Failed to unpin participant ${userId}`, err);
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
const toggleFullscreenMode = () => {
|
|
473
|
+
if (!fullscreenModeOn) {
|
|
474
|
+
return participantViewElement?.requestFullscreen().catch(console.error);
|
|
475
|
+
}
|
|
476
|
+
return document.exitFullscreen().catch(console.error);
|
|
477
|
+
};
|
|
478
|
+
react.useEffect(() => {
|
|
479
|
+
// handles the case when fullscreen mode is toggled externally,
|
|
480
|
+
// e.g., by pressing ESC key or some other keyboard shortcut
|
|
481
|
+
const handleFullscreenChange = () => {
|
|
482
|
+
setFullscreenModeOn(!!document.fullscreenElement);
|
|
483
|
+
};
|
|
484
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
485
|
+
return () => {
|
|
486
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
487
|
+
};
|
|
488
|
+
}, []);
|
|
489
|
+
const togglePictureInPicture = () => {
|
|
490
|
+
if (videoElement && !isPiP) {
|
|
491
|
+
return videoElement
|
|
492
|
+
.requestPictureInPicture()
|
|
493
|
+
.catch(console.error);
|
|
494
|
+
}
|
|
495
|
+
return document.exitPictureInPicture().catch(console.error);
|
|
496
|
+
};
|
|
497
|
+
const { close } = useMenuContext() || {};
|
|
498
|
+
return (jsxRuntime.jsxs(GenericMenu, { onItemClick: close, children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.PIN_FOR_EVERYONE], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsxRuntime.jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.KICK_USER], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: kickUser, children: [jsxRuntime.jsx(Icon, { icon: "kick-user" }), t('Kick')] }) }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement &&
|
|
499
|
+
typeof participantViewElement.requestFullscreen !== 'undefined' && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
|
|
500
|
+
direction: fullscreenModeOn ? t('Leave') : t('Enter'),
|
|
501
|
+
}) })), videoElement && document.pictureInPictureEnabled && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
|
|
502
|
+
direction: isPiP ? t('Leave') : t('Enter'),
|
|
503
|
+
}) })), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const isComponentType = (elementOrComponent) => {
|
|
507
|
+
return elementOrComponent === null
|
|
508
|
+
? false
|
|
509
|
+
: !react.isValidElement(elementOrComponent);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const chunk = (array, size) => {
|
|
513
|
+
const chunkCount = Math.ceil(array.length / size);
|
|
514
|
+
return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const applyElementToRef = (ref, element) => {
|
|
518
|
+
if (!ref)
|
|
519
|
+
return;
|
|
520
|
+
if (typeof ref === 'function')
|
|
521
|
+
return ref(element);
|
|
522
|
+
ref.current = element;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Normalizes a string for diacritic-insensitive comparison.
|
|
527
|
+
* E.g., "Éva" becomes "eva", allowing "eva" to match "Éva".
|
|
528
|
+
*/
|
|
529
|
+
const normalizeString = (value) => value
|
|
530
|
+
.normalize('NFD')
|
|
531
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
532
|
+
.toLowerCase();
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @description Extends video element with `stream` property
|
|
536
|
+
* (`srcObject`) to reactively handle stream changes
|
|
537
|
+
*/
|
|
538
|
+
const BaseVideo = react.forwardRef(function BaseVideo({ stream, ...rest }, ref) {
|
|
539
|
+
const [videoElement, setVideoElement] = react.useState(null);
|
|
540
|
+
react.useEffect(() => {
|
|
541
|
+
if (!videoElement || !stream)
|
|
542
|
+
return;
|
|
543
|
+
if (stream === videoElement.srcObject)
|
|
544
|
+
return;
|
|
545
|
+
videoElement.srcObject = stream;
|
|
546
|
+
if (videoClient.Browsers.isSafari() || videoClient.Browsers.isFirefox()) {
|
|
547
|
+
// Firefox and Safari have some timing issue
|
|
548
|
+
setTimeout(() => {
|
|
549
|
+
videoElement.srcObject = stream;
|
|
550
|
+
videoElement.play().catch((e) => {
|
|
551
|
+
console.error(`Failed to play stream`, e);
|
|
552
|
+
});
|
|
553
|
+
}, 0);
|
|
554
|
+
}
|
|
555
|
+
return () => {
|
|
556
|
+
videoElement.pause();
|
|
557
|
+
videoElement.srcObject = null;
|
|
558
|
+
};
|
|
559
|
+
}, [stream, videoElement]);
|
|
560
|
+
return (jsxRuntime.jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
|
|
561
|
+
applyElementToRef(ref, element);
|
|
562
|
+
setVideoElement(element);
|
|
563
|
+
} }));
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const BaseVideoPlaceholder = react.forwardRef(function DefaultVideoPlaceholder({ participant, style, children }, ref) {
|
|
567
|
+
const [error, setError] = react.useState(false);
|
|
568
|
+
const name = participant.name || participant.userId;
|
|
569
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
|
|
570
|
+
(name ? (jsxRuntime.jsx(InitialsFallback, { name: name })) : (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__no-video-label", children: children }))), participant.image && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: name, className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
|
|
571
|
+
});
|
|
572
|
+
const InitialsFallback = (props) => {
|
|
573
|
+
const { name } = props;
|
|
574
|
+
const initials = name
|
|
575
|
+
.split(' ')
|
|
576
|
+
.slice(0, 2)
|
|
577
|
+
.map((n) => n[0])
|
|
578
|
+
.join('');
|
|
579
|
+
return (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: initials }));
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const DefaultVideoPlaceholder = react.forwardRef(function DefaultVideoPlaceholder(props, ref) {
|
|
583
|
+
const { t } = videoReactBindings.useI18n();
|
|
584
|
+
return (jsxRuntime.jsx(BaseVideoPlaceholder, { ref: ref, ...props, children: t('Video is disabled') }));
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const DefaultPictureInPicturePlaceholder = react.forwardRef(function DefaultPictureInPicturePlaceholder(props, ref) {
|
|
588
|
+
const { t } = videoReactBindings.useI18n();
|
|
589
|
+
return (jsxRuntime.jsx(BaseVideoPlaceholder, { ref: ref, ...props, children: t('Video is playing in a popup') }));
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const Video$1 = ({ enabled = true, mirror, trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, PictureInPicturePlaceholder = DefaultPictureInPicturePlaceholder, refs, ...rest }) => {
|
|
593
|
+
const { sessionId, videoStream, screenShareStream, viewportVisibilityState, isLocalParticipant, userId, } = participant;
|
|
594
|
+
const call = videoReactBindings.useCall();
|
|
595
|
+
const [videoElement, setVideoElement] = react.useState(null);
|
|
596
|
+
// start with true, will flip once the video starts playing
|
|
597
|
+
const [isVideoPaused, setIsVideoPaused] = react.useState(true);
|
|
598
|
+
const [isWideMode, setIsWideMode] = react.useState(true);
|
|
599
|
+
const isPiP = usePictureInPictureState(videoElement ?? undefined);
|
|
600
|
+
const stream = trackType === 'videoTrack'
|
|
601
|
+
? videoStream
|
|
602
|
+
: trackType === 'screenShareTrack'
|
|
603
|
+
? screenShareStream
|
|
604
|
+
: undefined;
|
|
605
|
+
react.useLayoutEffect(() => {
|
|
606
|
+
if (!call || !videoElement || trackType === 'none')
|
|
607
|
+
return;
|
|
608
|
+
const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
|
|
609
|
+
return () => {
|
|
610
|
+
cleanup?.();
|
|
611
|
+
};
|
|
612
|
+
}, [call, trackType, sessionId, videoElement]);
|
|
613
|
+
react.useEffect(() => {
|
|
614
|
+
if (!stream || !videoElement)
|
|
615
|
+
return;
|
|
616
|
+
const [track] = stream.getVideoTracks();
|
|
617
|
+
if (!track)
|
|
618
|
+
return;
|
|
619
|
+
const handlePlayPause = () => {
|
|
620
|
+
setIsVideoPaused(videoElement.paused);
|
|
621
|
+
const { width = 0, height = 0 } = track.getSettings();
|
|
622
|
+
setIsWideMode(width >= height);
|
|
623
|
+
};
|
|
624
|
+
// playback may have started before we had a chance to
|
|
625
|
+
// attach the 'play/pause' event listener, so we set the state
|
|
626
|
+
// here to make sure it's in sync
|
|
627
|
+
setIsVideoPaused(videoElement.paused);
|
|
628
|
+
videoElement.addEventListener('play', handlePlayPause);
|
|
629
|
+
videoElement.addEventListener('pause', handlePlayPause);
|
|
630
|
+
track.addEventListener('unmute', handlePlayPause);
|
|
631
|
+
return () => {
|
|
632
|
+
videoElement.removeEventListener('play', handlePlayPause);
|
|
633
|
+
videoElement.removeEventListener('pause', handlePlayPause);
|
|
634
|
+
track.removeEventListener('unmute', handlePlayPause);
|
|
635
|
+
// reset the 'pause' state once we unmount the video element
|
|
636
|
+
setIsVideoPaused(true);
|
|
637
|
+
};
|
|
638
|
+
}, [stream, videoElement]);
|
|
639
|
+
if (!call)
|
|
640
|
+
return null;
|
|
641
|
+
const isPublishingTrack = trackType === 'videoTrack'
|
|
642
|
+
? videoClient.hasVideo(participant)
|
|
643
|
+
: trackType === 'screenShareTrack'
|
|
644
|
+
? videoClient.hasScreenShare(participant)
|
|
645
|
+
: false;
|
|
646
|
+
const isInvisible = trackType === 'none' ||
|
|
647
|
+
viewportVisibilityState?.[trackType] === videoClient.VisibilityState.INVISIBLE;
|
|
648
|
+
const hasNoVideoOrInvisible = !enabled ||
|
|
649
|
+
!isPublishingTrack ||
|
|
650
|
+
isInvisible ||
|
|
651
|
+
videoClient.hasPausedTrack(participant, trackType);
|
|
652
|
+
const mirrorVideo = mirror === undefined
|
|
653
|
+
? isLocalParticipant && trackType === 'videoTrack'
|
|
654
|
+
: mirror;
|
|
655
|
+
const isScreenShareTrack = trackType === 'screenShareTrack';
|
|
656
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!hasNoVideoOrInvisible && (jsxRuntime.jsx("video", { ...rest, className: clsx('str-video__video', className, {
|
|
657
|
+
'str-video__video--not-playing': isVideoPaused,
|
|
658
|
+
'str-video__video--tall': !isWideMode,
|
|
659
|
+
'str-video__video--mirror': mirrorVideo,
|
|
660
|
+
'str-video__video--screen-share': isScreenShareTrack,
|
|
661
|
+
}), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
|
|
662
|
+
setVideoElement(element);
|
|
663
|
+
refs?.setVideoElement?.(element);
|
|
664
|
+
} })), isPiP && PictureInPicturePlaceholder && (jsxRuntime.jsx(PictureInPicturePlaceholder, { style: { position: 'absolute' }, participant: participant })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
|
|
665
|
+
};
|
|
666
|
+
Video$1.displayName = 'Video';
|
|
667
|
+
|
|
668
|
+
const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
|
|
669
|
+
const call = videoReactBindings.useCall();
|
|
670
|
+
const manager = propsDynascaleManager ?? call?.dynascaleManager;
|
|
671
|
+
react.useEffect(() => {
|
|
672
|
+
if (!trackedElement || !manager || !call || trackType === 'none')
|
|
673
|
+
return;
|
|
674
|
+
const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
|
|
675
|
+
return () => {
|
|
676
|
+
unobserve();
|
|
677
|
+
};
|
|
678
|
+
}, [trackedElement, manager, call, sessionId, trackType]);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
|
|
682
|
+
const [error, setError] = react.useState(false);
|
|
683
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(!imageSrc || error) && name && (jsxRuntime.jsx(AvatarFallback, { className: className, style: style, names: [name] })), imageSrc && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "avatar", className: clsx('str-video__avatar', className), src: imageSrc, style: style, ...rest }))] }));
|
|
684
|
+
};
|
|
685
|
+
const AvatarFallback = ({ className, names, style, }) => {
|
|
686
|
+
return (jsxRuntime.jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxRuntime.jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const BackgroundFiltersProviderImpl = react.lazy(() => Promise.resolve().then(function () { return require('./embedded-BackgroundFilters-Zu84SkRR.cjs.js'); }).then((m) => ({
|
|
690
|
+
default: m.BackgroundFiltersProvider,
|
|
691
|
+
})));
|
|
692
|
+
/**
|
|
693
|
+
* The context for the background filters.
|
|
694
|
+
*/
|
|
695
|
+
const BackgroundFiltersContext = react.createContext(undefined);
|
|
696
|
+
/**
|
|
697
|
+
* A hook to access the background filters context API.
|
|
698
|
+
*/
|
|
699
|
+
const useBackgroundFilters = () => {
|
|
700
|
+
const context = react.useContext(BackgroundFiltersContext);
|
|
701
|
+
if (!context) {
|
|
702
|
+
throw new Error('useBackgroundFilters must be used within a BackgroundFiltersProvider');
|
|
703
|
+
}
|
|
704
|
+
return context;
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* A provider component that enables the use of background filters in your app.
|
|
708
|
+
*
|
|
709
|
+
* Please make sure you have the `@stream-io/video-filters-web` package installed
|
|
710
|
+
* in your project before using this component.
|
|
711
|
+
*/
|
|
712
|
+
const BackgroundFiltersProvider = (props) => {
|
|
713
|
+
const { SuspenseFallback = null, ...filterProps } = props;
|
|
714
|
+
return (jsxRuntime.jsx(react.Suspense, { fallback: SuspenseFallback, children: jsxRuntime.jsx(BackgroundFiltersProviderImpl, { ...filterProps, ContextProvider: BackgroundFiltersContext }) }));
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const IconButton = react.forwardRef(function IconButton(props, ref) {
|
|
718
|
+
const { icon, enabled, variant, onClick, className, ...rest } = props;
|
|
719
|
+
return (jsxRuntime.jsx("button", { className: clsx('str-video__call-controls__button', className, {
|
|
720
|
+
[`str-video__call-controls__button--variant-${variant}`]: variant,
|
|
721
|
+
'str-video__call-controls__button--enabled': enabled,
|
|
722
|
+
}), onClick: (e) => {
|
|
723
|
+
e.preventDefault();
|
|
724
|
+
onClick?.(e);
|
|
725
|
+
}, ref: ref, ...rest, children: jsxRuntime.jsx(Icon, { icon: icon }) }));
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const CompositeButton = react.forwardRef(function CompositeButton({ disabled, caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
|
|
729
|
+
return (jsxRuntime.jsxs("div", { className: clsx('str-video__composite-button', className, {
|
|
730
|
+
'str-video__composite-button--caption': caption,
|
|
731
|
+
'str-video__composite-button--menu': Menu,
|
|
732
|
+
}), title: title, ref: ref, children: [jsxRuntime.jsxs("div", { className: clsx('str-video__composite-button__button-group', {
|
|
733
|
+
'str-video__composite-button__button-group--active': active,
|
|
734
|
+
'str-video__composite-button__button-group--active-primary': active && variant === 'primary',
|
|
735
|
+
'str-video__composite-button__button-group--active-secondary': active && variant === 'secondary',
|
|
736
|
+
'str-video__composite-button__button-group--disabled': disabled,
|
|
737
|
+
}), children: [jsxRuntime.jsx("button", { type: "button", className: "str-video__composite-button__button", onClick: (e) => {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
onClick?.(e);
|
|
740
|
+
}, disabled: disabled, ...restButtonProps, children: children }), Menu && (jsxRuntime.jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
|
|
741
|
+
});
|
|
742
|
+
const DefaultToggleMenuButton = react.forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
|
|
743
|
+
return (jsxRuntime.jsx(IconButton, { className: clsx('str-video__menu-toggle-button', {
|
|
744
|
+
'str-video__menu-toggle-button--active': menuShown,
|
|
745
|
+
}), icon: menuShown ? 'caret-down' : 'caret-up', ref: ref }));
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const TextButton = ({ children, ...rest }) => {
|
|
749
|
+
return (jsxRuntime.jsx("button", { ...rest, className: "str-video__text-button", children: children }));
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const Notification = (props) => {
|
|
753
|
+
const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', className, iconClassName = 'str-video__notification__icon', close, } = props;
|
|
754
|
+
const { refs, x, y, strategy } = useFloatingUIPreset({
|
|
755
|
+
placement,
|
|
756
|
+
strategy: 'absolute',
|
|
757
|
+
});
|
|
758
|
+
react.useEffect(() => {
|
|
759
|
+
if (!isVisible || !visibilityTimeout || !resetIsVisible)
|
|
760
|
+
return;
|
|
761
|
+
const timeout = setTimeout(() => {
|
|
762
|
+
resetIsVisible();
|
|
763
|
+
}, visibilityTimeout);
|
|
764
|
+
return () => clearTimeout(timeout);
|
|
765
|
+
}, [isVisible, resetIsVisible, visibilityTimeout]);
|
|
766
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__notification-wrapper", ref: refs.setReference, children: [isVisible && (jsxRuntime.jsxs("div", { className: clsx('str-video__notification', className), ref: refs.setFloating, style: {
|
|
767
|
+
position: strategy,
|
|
768
|
+
top: y ?? 0,
|
|
769
|
+
left: x ?? 0,
|
|
770
|
+
overflowY: 'auto',
|
|
771
|
+
}, children: [iconClassName && jsxRuntime.jsx("i", { className: iconClassName }), jsxRuntime.jsx("span", { className: "str-video__notification__message", children: message }), close ? (jsxRuntime.jsx("i", { className: "str-video__icon str-video__icon--close str-video__notification__close", onClick: close })) : null] })), children] }));
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const PermissionNotification = (props) => {
|
|
775
|
+
const { permission, isAwaitingApproval, messageApproved, messageAwaitingApproval, messageRevoked, visibilityTimeout = 3500, children, } = props;
|
|
776
|
+
const { useHasPermissions } = videoReactBindings.useCallStateHooks();
|
|
777
|
+
const hasPermission = useHasPermissions(permission);
|
|
778
|
+
const prevHasPermission = react.useRef(hasPermission);
|
|
779
|
+
const [showNotification, setShowNotification] = react.useState();
|
|
780
|
+
react.useEffect(() => {
|
|
781
|
+
if (prevHasPermission.current === hasPermission)
|
|
782
|
+
return;
|
|
783
|
+
if (hasPermission) {
|
|
784
|
+
setShowNotification('granted');
|
|
785
|
+
prevHasPermission.current = true;
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
setShowNotification('revoked');
|
|
789
|
+
prevHasPermission.current = false;
|
|
790
|
+
}
|
|
791
|
+
}, [hasPermission]);
|
|
792
|
+
const resetIsVisible = react.useCallback(() => setShowNotification(undefined), []);
|
|
793
|
+
if (isAwaitingApproval) {
|
|
794
|
+
return (jsxRuntime.jsx(Notification, { isVisible: isAwaitingApproval && !hasPermission, message: messageAwaitingApproval, children: children }));
|
|
795
|
+
}
|
|
796
|
+
return (jsxRuntime.jsx(Notification, { isVisible: !!showNotification, visibilityTimeout: visibilityTimeout, resetIsVisible: resetIsVisible, message: showNotification === 'granted' ? messageApproved : messageRevoked, children: children }));
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const SpeakingWhileMutedNotification = ({ children, text, placement, }) => {
|
|
800
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
801
|
+
const { isSpeakingWhileMuted } = useMicrophoneState();
|
|
802
|
+
const { t } = videoReactBindings.useI18n();
|
|
803
|
+
const message = text ?? t('You are muted. Unmute to speak.');
|
|
804
|
+
return (jsxRuntime.jsx(Notification, { message: message, isVisible: isSpeakingWhileMuted, placement: placement || 'top-start', children: children }));
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const MicCaptureErrorNotification = ({ children, text, placement, }) => {
|
|
808
|
+
const call = videoReactBindings.useCall();
|
|
809
|
+
const { t } = videoReactBindings.useI18n();
|
|
810
|
+
const [isVisible, setIsVisible] = react.useState(false);
|
|
811
|
+
react.useEffect(() => {
|
|
812
|
+
if (!call)
|
|
813
|
+
return;
|
|
814
|
+
return call.on('mic.capture_report', (event) => {
|
|
815
|
+
setIsVisible(!event.capturesAudio);
|
|
816
|
+
});
|
|
817
|
+
}, [call]);
|
|
818
|
+
const message = text ??
|
|
819
|
+
t('Your microphone is not capturing audio. Please check your setup.');
|
|
820
|
+
return (jsxRuntime.jsx(Notification, { message: message, isVisible: isVisible, placement: placement, close: () => setIsVisible(false), children: children }));
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const LoadingIndicator = ({ className, type = 'spinner', text, tooltip, }) => {
|
|
824
|
+
return (jsxRuntime.jsxs("div", { className: clsx('str-video__loading-indicator', className), title: tooltip, children: [jsxRuntime.jsx("div", { className: clsx('str-video__loading-indicator__icon', type) }), text && jsxRuntime.jsx("p", { className: "str-video__loading-indicator-text", children: text })] }));
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
|
|
828
|
+
const arrowRef = react.useRef(null);
|
|
829
|
+
const { refs, x, y, strategy, context } = useFloatingUIPreset({
|
|
830
|
+
placement: tooltipPlacement,
|
|
831
|
+
strategy: 'absolute',
|
|
832
|
+
middleware: [react$1.arrow({ element: arrowRef })],
|
|
833
|
+
});
|
|
834
|
+
react.useEffect(() => {
|
|
835
|
+
refs.setReference(referenceElement);
|
|
836
|
+
}, [referenceElement, refs]);
|
|
837
|
+
if (!visible)
|
|
838
|
+
return null;
|
|
839
|
+
return (jsxRuntime.jsxs("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
|
|
840
|
+
position: strategy,
|
|
841
|
+
top: y ?? 0,
|
|
842
|
+
left: x ?? 0,
|
|
843
|
+
}, children: [jsxRuntime.jsx(react$1.FloatingArrow, { ref: arrowRef, context: context, fill: "var(--str-video__tooltip--background-color)" }), children] }));
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
|
|
847
|
+
const [tooltipVisible, setTooltipVisible] = react.useState(false);
|
|
848
|
+
const handleMouseEnter = react.useCallback((e) => {
|
|
849
|
+
setTooltipVisible(true);
|
|
850
|
+
onMouseEnter?.(e);
|
|
851
|
+
}, [onMouseEnter]);
|
|
852
|
+
const handleMouseLeave = react.useCallback((e) => {
|
|
853
|
+
setTooltipVisible(false);
|
|
854
|
+
onMouseLeave?.(e);
|
|
855
|
+
}, [onMouseLeave]);
|
|
856
|
+
return { handleMouseEnter, handleMouseLeave, tooltipVisible };
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
|
|
860
|
+
const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, tooltipDisabled, ...props }) => {
|
|
861
|
+
const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
|
|
862
|
+
const [tooltipAnchor, setTooltipAnchor] = react.useState(null);
|
|
863
|
+
const tooltipActuallyVisible = !tooltipDisabled && Boolean(title) && tooltipVisible;
|
|
864
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipActuallyVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsxRuntime.jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const RecordEndConfirmation = () => {
|
|
868
|
+
const { t } = videoReactBindings.useI18n();
|
|
869
|
+
const { toggleCallRecording, isAwaitingResponse } = videoReactBindings.useToggleCallRecording();
|
|
870
|
+
const { close } = useMenuContext();
|
|
871
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__end-recording__confirmation", children: [jsxRuntime.jsxs("div", { className: "str-video__end-recording__header", children: [jsxRuntime.jsx(Icon, { icon: "recording-on" }), jsxRuntime.jsx("h2", { className: "str-video__end-recording__heading", children: t('End recording') })] }), jsxRuntime.jsx("p", { className: "str-video__end-recording__description", children: t('Are you sure you want end the recording?') }), jsxRuntime.jsxs("div", { className: "str-video__end-recording__actions", children: [jsxRuntime.jsx(CompositeButton, { variant: "secondary", onClick: close, children: t('Cancel') }), jsxRuntime.jsx(CompositeButton, { variant: "primary", onClick: toggleCallRecording, children: isAwaitingResponse ? jsxRuntime.jsx(LoadingIndicator, {}) : t('End recording') })] })] }));
|
|
872
|
+
};
|
|
873
|
+
const ToggleEndRecordingMenuButton = react.forwardRef(function ToggleEndRecordingMenuButton(props, ref) {
|
|
874
|
+
return (jsxRuntime.jsx(CompositeButton, { ref: ref, active: true, variant: "secondary", "data-testid": "recording-stop-button", children: jsxRuntime.jsx(Icon, { icon: "recording-off" }) }));
|
|
875
|
+
});
|
|
876
|
+
const RecordCallConfirmationButton = ({ caption, }) => {
|
|
877
|
+
const { t } = videoReactBindings.useI18n();
|
|
878
|
+
const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = videoReactBindings.useToggleCallRecording();
|
|
879
|
+
if (isCallRecordingInProgress) {
|
|
880
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [
|
|
881
|
+
videoClient.OwnCapability.START_RECORD_CALL,
|
|
882
|
+
videoClient.OwnCapability.STOP_RECORD_CALL,
|
|
883
|
+
], children: jsxRuntime.jsx(MenuToggle, { ToggleButton: ToggleEndRecordingMenuButton, visualType: MenuVisualType.PORTAL, children: jsxRuntime.jsx(RecordEndConfirmation, {}) }) }));
|
|
884
|
+
}
|
|
885
|
+
const title = isAwaitingResponse
|
|
886
|
+
? t('Waiting for recording to start...')
|
|
887
|
+
: (caption ?? t('Record call'));
|
|
888
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [
|
|
889
|
+
videoClient.OwnCapability.START_RECORD_CALL,
|
|
890
|
+
videoClient.OwnCapability.STOP_RECORD_CALL,
|
|
891
|
+
], children: jsxRuntime.jsx(WithTooltip, { title: title, children: jsxRuntime.jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, variant: "secondary", "data-testid": "recording-start-button", onClick: isAwaitingResponse ? undefined : toggleCallRecording, children: isAwaitingResponse ? (jsxRuntime.jsx(LoadingIndicator, {})) : (jsxRuntime.jsx(Icon, { icon: "recording-off" })) }) }) }));
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const defaultEmojiReactionMap = {
|
|
895
|
+
':like:': '👍',
|
|
896
|
+
':raise-hand:': '✋',
|
|
897
|
+
':fireworks:': '🎉',
|
|
898
|
+
':dislike:': '👎',
|
|
899
|
+
':heart:': '❤️',
|
|
900
|
+
':smile:': '😀',
|
|
901
|
+
};
|
|
902
|
+
const Reaction = ({ participant: { reaction, sessionId }, hideAfterTimeoutInMs = 5500, emojiReactionMap = defaultEmojiReactionMap, }) => {
|
|
903
|
+
const call = videoReactBindings.useCall();
|
|
904
|
+
react.useEffect(() => {
|
|
905
|
+
if (!call || !reaction)
|
|
906
|
+
return;
|
|
907
|
+
const timeoutId = setTimeout(() => {
|
|
908
|
+
call.resetReaction(sessionId);
|
|
909
|
+
}, hideAfterTimeoutInMs);
|
|
910
|
+
return () => {
|
|
911
|
+
clearTimeout(timeoutId);
|
|
912
|
+
};
|
|
913
|
+
}, [call, hideAfterTimeoutInMs, reaction, sessionId]);
|
|
914
|
+
if (!reaction)
|
|
915
|
+
return null;
|
|
916
|
+
const { emoji_code: emojiCode } = reaction;
|
|
917
|
+
return (jsxRuntime.jsx("div", { className: "str-video__reaction", children: jsxRuntime.jsx("span", { className: "str-video__reaction__emoji", children: emojiCode && emojiReactionMap[emojiCode] }) }));
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const defaultReactions = [
|
|
921
|
+
{
|
|
922
|
+
type: 'reaction',
|
|
923
|
+
emoji_code: ':like:',
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
// TODO OL: use `prompt` type?
|
|
927
|
+
type: 'raised-hand',
|
|
928
|
+
emoji_code: ':raise-hand:',
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
type: 'reaction',
|
|
932
|
+
emoji_code: ':fireworks:',
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
type: 'reaction',
|
|
936
|
+
emoji_code: ':dislike:',
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
type: 'reaction',
|
|
940
|
+
emoji_code: ':heart:',
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
type: 'reaction',
|
|
944
|
+
emoji_code: ':smile:',
|
|
945
|
+
},
|
|
946
|
+
];
|
|
947
|
+
const ReactionsButton = ({ reactions = defaultReactions, }) => {
|
|
948
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx(MenuToggle, { placement: "top", ToggleButton: ToggleReactionsMenuButton, visualType: MenuVisualType.MENU, children: jsxRuntime.jsx(DefaultReactionsMenu, { reactions: reactions }) }) }));
|
|
949
|
+
};
|
|
950
|
+
const ToggleReactionsMenuButton = react.forwardRef(function ToggleReactionsMenuButton({ menuShown }, ref) {
|
|
951
|
+
const { t } = videoReactBindings.useI18n();
|
|
952
|
+
return (jsxRuntime.jsx(WithTooltip, { title: t('Reactions'), tooltipDisabled: menuShown, children: jsxRuntime.jsx(CompositeButton, { ref: ref, active: menuShown, variant: "primary", children: jsxRuntime.jsx(Icon, { icon: "reactions" }) }) }));
|
|
953
|
+
});
|
|
954
|
+
const DefaultReactionsMenu = ({ reactions, layout = 'horizontal', }) => {
|
|
955
|
+
const call = videoReactBindings.useCall();
|
|
956
|
+
const { close } = useMenuContext();
|
|
957
|
+
return (jsxRuntime.jsx("div", { className: clsx('str-video__reactions-menu', {
|
|
958
|
+
'str-video__reactions-menu--horizontal': layout === 'horizontal',
|
|
959
|
+
'str-video__reactions-menu--vertical': layout === 'vertical',
|
|
960
|
+
}), children: reactions.map((reaction) => (jsxRuntime.jsx("button", { type: "button", className: "str-video__reactions-menu__button", onClick: () => {
|
|
961
|
+
call?.sendReaction(reaction);
|
|
962
|
+
close?.();
|
|
963
|
+
}, children: reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code] }, reaction.emoji_code))) }));
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Wraps an event handler, silencing and logging exceptions (excluding the NotAllowedError
|
|
968
|
+
* DOMException, which is a normal situation handled by the SDK)
|
|
969
|
+
*
|
|
970
|
+
* @param props component props, including the onError callback
|
|
971
|
+
* @param handler event handler to wrap
|
|
972
|
+
*/
|
|
973
|
+
const createCallControlHandler = (props, handler) => {
|
|
974
|
+
return async () => {
|
|
975
|
+
try {
|
|
976
|
+
await handler();
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
if (props.onError) {
|
|
980
|
+
props.onError(error);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!isNotAllowedError(error)) {
|
|
984
|
+
console.error('Call control handler failed', error);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
function isNotAllowedError(error) {
|
|
990
|
+
return error instanceof DOMException && error.name === 'NotAllowedError';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const ScreenShareButton = (props) => {
|
|
994
|
+
const { t } = videoReactBindings.useI18n();
|
|
995
|
+
const { caption, optimisticUpdates } = props;
|
|
996
|
+
const { useHasOngoingScreenShare, useScreenShareState, useCallSettings } = videoReactBindings.useCallStateHooks();
|
|
997
|
+
const isSomeoneScreenSharing = useHasOngoingScreenShare();
|
|
998
|
+
const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SCREENSHARE);
|
|
999
|
+
const callSettings = useCallSettings();
|
|
1000
|
+
const isScreenSharingAllowed = callSettings?.screensharing.enabled;
|
|
1001
|
+
const { screenShare, optionsAwareIsMute, isTogglePending } = useScreenShareState({
|
|
1002
|
+
optimisticUpdates,
|
|
1003
|
+
});
|
|
1004
|
+
const amIScreenSharing = !optionsAwareIsMute;
|
|
1005
|
+
const disableScreenShareButton = (!amIScreenSharing &&
|
|
1006
|
+
(isSomeoneScreenSharing || isScreenSharingAllowed === false)) ||
|
|
1007
|
+
(!optimisticUpdates && isTogglePending);
|
|
1008
|
+
const handleClick = createCallControlHandler(props, async () => {
|
|
1009
|
+
if (!hasPermission) {
|
|
1010
|
+
await requestPermission();
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
await screenShare.toggle();
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SCREENSHARE], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SCREENSHARE, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your screen.'), messageAwaitingApproval: t('Awaiting for an approval to share screen.'), messageRevoked: t('You can no longer share your screen.'), children: jsxRuntime.jsx(WithTooltip, { title: caption ?? t('Share screen'), children: jsxRuntime.jsx(CompositeButton, { active: isSomeoneScreenSharing || amIScreenSharing, caption: caption, variant: "primary", "data-testid": isSomeoneScreenSharing
|
|
1017
|
+
? 'screen-share-stop-button'
|
|
1018
|
+
: 'screen-share-start-button', disabled: disableScreenShareButton, onClick: handleClick, children: jsxRuntime.jsx(Icon, { icon: isSomeoneScreenSharing ? 'screen-share-on' : 'screen-share-off' }) }) }) }) }));
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const AudioVolumeIndicator = () => {
|
|
1022
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
1023
|
+
const { isEnabled, mediaStream } = useMicrophoneState();
|
|
1024
|
+
const [audioLevel, setAudioLevel] = react.useState(0);
|
|
1025
|
+
react.useEffect(() => {
|
|
1026
|
+
if (!isEnabled || !mediaStream)
|
|
1027
|
+
return;
|
|
1028
|
+
const disposeSoundDetector = videoClient.createSoundDetector(mediaStream, ({ audioLevel: al }) => setAudioLevel(al), { detectionFrequencyInMs: 80, destroyStreamOnStop: false });
|
|
1029
|
+
return () => {
|
|
1030
|
+
disposeSoundDetector().catch(console.error);
|
|
1031
|
+
};
|
|
1032
|
+
}, [isEnabled, mediaStream]);
|
|
1033
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__audio-volume-indicator", children: [jsxRuntime.jsx(Icon, { icon: isEnabled ? 'mic' : 'mic-off' }), jsxRuntime.jsx("div", { className: "str-video__audio-volume-indicator__bar", children: jsxRuntime.jsx("div", { className: "str-video__audio-volume-indicator__bar-value", style: { transform: `scaleX(${audioLevel / 100})` } }) })] }));
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const SelectContext = react.createContext({});
|
|
1037
|
+
const Select = (props) => {
|
|
1038
|
+
const { children, icon, defaultSelectedLabel, defaultSelectedIndex, handleSelect: handleSelectProp, } = props;
|
|
1039
|
+
const [isOpen, setIsOpen] = react.useState(false);
|
|
1040
|
+
const [activeIndex, setActiveIndex] = react.useState(null);
|
|
1041
|
+
const [selectedIndex, setSelectedIndex] = react.useState(defaultSelectedIndex);
|
|
1042
|
+
const [selectedLabel, setSelectedLabel] = react.useState(defaultSelectedLabel);
|
|
1043
|
+
const { refs, context } = react$1.useFloating({
|
|
1044
|
+
placement: 'bottom-start',
|
|
1045
|
+
open: isOpen,
|
|
1046
|
+
onOpenChange: setIsOpen,
|
|
1047
|
+
whileElementsMounted: react$1.autoUpdate,
|
|
1048
|
+
middleware: [react$1.flip()],
|
|
1049
|
+
});
|
|
1050
|
+
const elementsRef = react.useRef([]);
|
|
1051
|
+
const labelsRef = react.useRef([]);
|
|
1052
|
+
const handleSelect = react.useCallback((index) => {
|
|
1053
|
+
setSelectedIndex(index);
|
|
1054
|
+
handleSelectProp(index || 0);
|
|
1055
|
+
setIsOpen(false);
|
|
1056
|
+
if (index !== null) {
|
|
1057
|
+
setSelectedLabel(labelsRef.current[index]);
|
|
1058
|
+
}
|
|
1059
|
+
}, [handleSelectProp]);
|
|
1060
|
+
const handleTypeaheadMatch = (index) => {
|
|
1061
|
+
if (isOpen) {
|
|
1062
|
+
setActiveIndex(index);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
handleSelect(index);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const listNav = react$1.useListNavigation(context, {
|
|
1069
|
+
listRef: elementsRef,
|
|
1070
|
+
activeIndex,
|
|
1071
|
+
selectedIndex,
|
|
1072
|
+
onNavigate: setActiveIndex,
|
|
1073
|
+
});
|
|
1074
|
+
const typeahead = react$1.useTypeahead(context, {
|
|
1075
|
+
listRef: labelsRef,
|
|
1076
|
+
activeIndex,
|
|
1077
|
+
selectedIndex,
|
|
1078
|
+
onMatch: handleTypeaheadMatch,
|
|
1079
|
+
});
|
|
1080
|
+
const click = react$1.useClick(context);
|
|
1081
|
+
const dismiss = react$1.useDismiss(context);
|
|
1082
|
+
const role = react$1.useRole(context, { role: 'listbox' });
|
|
1083
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = react$1.useInteractions([listNav, typeahead, click, dismiss, role]);
|
|
1084
|
+
const selectContext = react.useMemo(() => ({
|
|
1085
|
+
activeIndex,
|
|
1086
|
+
selectedIndex,
|
|
1087
|
+
getItemProps,
|
|
1088
|
+
handleSelect,
|
|
1089
|
+
}), [activeIndex, selectedIndex, getItemProps, handleSelect]);
|
|
1090
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__dropdown", children: [jsxRuntime.jsxs("div", { className: "str-video__dropdown-selected", ref: refs.setReference, tabIndex: 0, ...getReferenceProps(), children: [jsxRuntime.jsxs("label", { className: "str-video__dropdown-selected__label", children: [icon && (jsxRuntime.jsx(Icon, { className: "str-video__dropdown-selected__icon", icon: icon })), selectedLabel] }), jsxRuntime.jsx(Icon, { className: "str-video__dropdown-selected__chevron", icon: isOpen ? 'chevron-up' : 'chevron-down' })] }), jsxRuntime.jsx(SelectContext.Provider, { value: selectContext, children: isOpen && (jsxRuntime.jsx(react$1.FloatingFocusManager, { context: context, modal: false, children: jsxRuntime.jsx("div", { className: "str-video__dropdown-list", ref: refs.setFloating, ...getFloatingProps(), children: jsxRuntime.jsx(react$1.FloatingList, { elementsRef: elementsRef, labelsRef: labelsRef, children: children }) }) })) })] }));
|
|
1091
|
+
};
|
|
1092
|
+
const DropDownSelectOption = (props) => {
|
|
1093
|
+
const { selected, label, icon } = props;
|
|
1094
|
+
const { getItemProps, handleSelect } = react.useContext(SelectContext);
|
|
1095
|
+
const { ref, index } = react$1.useListItem();
|
|
1096
|
+
return (jsxRuntime.jsxs("div", { className: clsx('str-video__dropdown-option', {
|
|
1097
|
+
'str-video__dropdown-option--selected': selected,
|
|
1098
|
+
}), ref: ref, ...getItemProps({
|
|
1099
|
+
onClick: () => handleSelect(index),
|
|
1100
|
+
}), children: [icon && jsxRuntime.jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsxRuntime.jsx("span", { className: "str-video__dropdown-label", children: label })] }));
|
|
1101
|
+
};
|
|
1102
|
+
const DropDownSelect = (props) => {
|
|
1103
|
+
const { children, icon, handleSelect, defaultSelectedLabel, defaultSelectedIndex, } = props;
|
|
1104
|
+
return (jsxRuntime.jsx(Select, { icon: icon, handleSelect: handleSelect, defaultSelectedIndex: defaultSelectedIndex, defaultSelectedLabel: defaultSelectedLabel, children: children }));
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
const DeviceSelectorOption = ({ disabled, id, label, onChange, name, selected, defaultChecked, value, }) => {
|
|
1108
|
+
return (jsxRuntime.jsxs("label", { className: clsx('str-video__device-settings__option', {
|
|
1109
|
+
'str-video__device-settings__option--selected': selected,
|
|
1110
|
+
'str-video__device-settings__option--disabled': disabled,
|
|
1111
|
+
}), htmlFor: id, children: [jsxRuntime.jsx("input", { type: "radio", name: name, onChange: onChange, value: value, id: id, checked: selected, defaultChecked: defaultChecked, disabled: disabled }), label] }));
|
|
1112
|
+
};
|
|
1113
|
+
const DeviceSelectorList = (props) => {
|
|
1114
|
+
const { devices = [], selectedDeviceId, title, type, onChange, children, } = props;
|
|
1115
|
+
const { close } = useMenuContext();
|
|
1116
|
+
const { deviceList } = useDeviceList(devices, selectedDeviceId);
|
|
1117
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__device-settings__device-kind", children: [title && (jsxRuntime.jsx("div", { className: "str-video__device-settings__device-selector-title", children: title })), deviceList.map((device) => {
|
|
1118
|
+
return (jsxRuntime.jsx(DeviceSelectorOption, { id: `${type}--${device.deviceId}`, value: device.deviceId, label: device.label, onChange: (e) => {
|
|
1119
|
+
const deviceId = e.target.value;
|
|
1120
|
+
if (deviceId !== 'default') {
|
|
1121
|
+
onChange?.(deviceId);
|
|
1122
|
+
}
|
|
1123
|
+
close?.();
|
|
1124
|
+
}, name: type, selected: device.isSelected }, device.deviceId));
|
|
1125
|
+
}), children] }));
|
|
1126
|
+
};
|
|
1127
|
+
const DeviceSelectorDropdown = (props) => {
|
|
1128
|
+
const { devices = [], selectedDeviceId, title, onChange, icon } = props;
|
|
1129
|
+
const { deviceList, selectedDeviceInfo, selectedIndex } = useDeviceList(devices, selectedDeviceId);
|
|
1130
|
+
const handleSelect = react.useCallback((index) => {
|
|
1131
|
+
const deviceId = deviceList[index].deviceId;
|
|
1132
|
+
if (deviceId !== 'default') {
|
|
1133
|
+
onChange?.(deviceId);
|
|
1134
|
+
}
|
|
1135
|
+
}, [deviceList, onChange]);
|
|
1136
|
+
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))) })] }));
|
|
1137
|
+
};
|
|
1138
|
+
const DeviceSelector = (props) => {
|
|
1139
|
+
const { visualType = 'list', icon, ...rest } = props;
|
|
1140
|
+
if (visualType === 'list') {
|
|
1141
|
+
return jsxRuntime.jsx(DeviceSelectorList, { ...rest });
|
|
1142
|
+
}
|
|
1143
|
+
return jsxRuntime.jsx(DeviceSelectorDropdown, { ...rest, icon: icon });
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* SpeakerTest component that plays a test audio through the selected speaker.
|
|
1148
|
+
* This allows users to verify their audio output device is working correctly.
|
|
1149
|
+
*/
|
|
1150
|
+
const SpeakerTest = (props) => {
|
|
1151
|
+
const { useSpeakerState } = videoReactBindings.useCallStateHooks();
|
|
1152
|
+
const { selectedDevice } = useSpeakerState();
|
|
1153
|
+
const audioElementRef = react.useRef(null);
|
|
1154
|
+
const [isPlaying, setIsPlaying] = react.useState(false);
|
|
1155
|
+
const { t } = videoReactBindings.useI18n();
|
|
1156
|
+
const { audioUrl = `https://unpkg.com/${"@stream-io/video-react-sdk"}@${"1.33.0"}/assets/piano.mp3`, } = props;
|
|
1157
|
+
// Update audio output device when selection changes
|
|
1158
|
+
react.useEffect(() => {
|
|
1159
|
+
const audio = audioElementRef.current;
|
|
1160
|
+
if (!audio || !selectedDevice)
|
|
1161
|
+
return;
|
|
1162
|
+
// Set the sinkId to route audio to the selected speaker
|
|
1163
|
+
if ('setSinkId' in audio) {
|
|
1164
|
+
audio.setSinkId(selectedDevice).catch((err) => {
|
|
1165
|
+
console.error('Failed to set audio output device:', err);
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}, [selectedDevice]);
|
|
1169
|
+
const handleStartTest = react.useCallback(async () => {
|
|
1170
|
+
const audio = audioElementRef.current;
|
|
1171
|
+
if (!audio)
|
|
1172
|
+
return;
|
|
1173
|
+
audio.src = audioUrl;
|
|
1174
|
+
try {
|
|
1175
|
+
if (isPlaying) {
|
|
1176
|
+
audio.pause();
|
|
1177
|
+
audio.currentTime = 0;
|
|
1178
|
+
setIsPlaying(false);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
await audio.play();
|
|
1182
|
+
setIsPlaying(true);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
catch (err) {
|
|
1186
|
+
console.error('Failed to play test audio:', err);
|
|
1187
|
+
setIsPlaying(false);
|
|
1188
|
+
}
|
|
1189
|
+
}, [isPlaying, audioUrl]);
|
|
1190
|
+
const handleAudioEnded = react.useCallback(() => setIsPlaying(false), []);
|
|
1191
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__speaker-test", children: [jsxRuntime.jsx("audio", { ref: audioElementRef, onEnded: handleAudioEnded, onPause: handleAudioEnded }), jsxRuntime.jsx(CompositeButton, { className: "str-video__speaker-test__button", onClick: handleStartTest, type: "button", children: jsxRuntime.jsxs("div", { className: "str-video__speaker-test__button-content", children: [jsxRuntime.jsx(Icon, { icon: "speaker" }), isPlaying ? t('Stop test') : t('Test speaker')] }) })] }));
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const DeviceSelectorAudioInput = ({ title, visualType, volumeIndicatorVisible = true, }) => {
|
|
1195
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
1196
|
+
const { microphone, selectedDevice, devices } = useMicrophoneState();
|
|
1197
|
+
return (jsxRuntime.jsx(DeviceSelector, { devices: devices || [], selectedDeviceId: selectedDevice, type: "audioinput", onChange: async (deviceId) => {
|
|
1198
|
+
await microphone.select(deviceId);
|
|
1199
|
+
}, title: title, visualType: visualType, icon: "mic", children: volumeIndicatorVisible && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("hr", { className: "str-video__device-settings__separator" }), jsxRuntime.jsx(AudioVolumeIndicator, {})] })) }));
|
|
1200
|
+
};
|
|
1201
|
+
const DeviceSelectorAudioOutput = ({ title, visualType, speakerTestVisible = true, speakerTestAudioUrl, }) => {
|
|
1202
|
+
const { useSpeakerState } = videoReactBindings.useCallStateHooks();
|
|
1203
|
+
const { speaker, selectedDevice, devices, isDeviceSelectionSupported } = useSpeakerState();
|
|
1204
|
+
if (!isDeviceSelectionSupported)
|
|
1205
|
+
return null;
|
|
1206
|
+
return (jsxRuntime.jsx(DeviceSelector, { devices: devices, type: "audiooutput", selectedDeviceId: selectedDevice, onChange: (deviceId) => {
|
|
1207
|
+
speaker.select(deviceId);
|
|
1208
|
+
}, title: title, visualType: visualType, icon: "speaker", children: speakerTestVisible && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("hr", { className: "str-video__device-settings__separator" }), jsxRuntime.jsx(SpeakerTest, { audioUrl: speakerTestAudioUrl })] })) }));
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
const DeviceSelectorVideo = ({ title, visualType, }) => {
|
|
1212
|
+
const { useCameraState } = videoReactBindings.useCallStateHooks();
|
|
1213
|
+
const { camera, devices, selectedDevice } = useCameraState();
|
|
1214
|
+
return (jsxRuntime.jsx(DeviceSelector, { devices: devices || [], type: "videoinput", selectedDeviceId: selectedDevice, onChange: async (deviceId) => {
|
|
1215
|
+
await camera.select(deviceId);
|
|
1216
|
+
}, title: title, visualType: visualType, icon: "camera" }));
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
react.forwardRef(function ToggleDeviceSettingsMenuButton({ menuShown }, ref) {
|
|
1220
|
+
const { t } = videoReactBindings.useI18n();
|
|
1221
|
+
return (jsxRuntime.jsx(IconButton, { className: clsx('str-video__device-settings__button', {
|
|
1222
|
+
'str-video__device-settings__button--active': menuShown,
|
|
1223
|
+
}), title: t('Toggle device menu'), icon: "device-settings", ref: ref }));
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const ToggleAudioPreviewButton = (props) => {
|
|
1227
|
+
const { caption, Menu = DeviceSelectorAudioInput, menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
|
|
1228
|
+
const { t } = videoReactBindings.useI18n();
|
|
1229
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
1230
|
+
const { microphone, hasBrowserPermission, isPromptingPermission, optionsAwareIsMute, isTogglePending, } = useMicrophoneState({ optimisticUpdates });
|
|
1231
|
+
const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
|
|
1232
|
+
const handleClick = createCallControlHandler(props, () => microphone.toggle());
|
|
1233
|
+
return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
|
|
1234
|
+
? t('Check your browser audio permissions')
|
|
1235
|
+
: (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", disabled: !hasBrowserPermission || (!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute
|
|
1236
|
+
? 'preview-audio-unmute-button'
|
|
1237
|
+
: 'preview-audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
|
|
1238
|
+
setTooltipDisabled(shown);
|
|
1239
|
+
onMenuToggle?.(shown);
|
|
1240
|
+
}, children: [jsxRuntime.jsx(Icon, { icon: !optionsAwareIsMute ? 'mic' : 'mic-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser audio permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
|
|
1241
|
+
};
|
|
1242
|
+
const ToggleAudioPublishingButton = (props) => {
|
|
1243
|
+
const { t } = videoReactBindings.useI18n();
|
|
1244
|
+
const { caption, Menu = jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
|
|
1245
|
+
const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SEND_AUDIO);
|
|
1246
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
1247
|
+
const { microphone, hasBrowserPermission, isPromptingPermission, isTogglePending, optionsAwareIsMute, } = useMicrophoneState({ optimisticUpdates });
|
|
1248
|
+
const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
|
|
1249
|
+
const handleClick = createCallControlHandler(props, async () => {
|
|
1250
|
+
if (!hasPermission) {
|
|
1251
|
+
await requestPermission();
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
await microphone.toggle();
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_AUDIO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now speak.'), messageAwaitingApproval: t('Awaiting for an approval to speak.'), messageRevoked: t('You can no longer speak.'), children: jsxRuntime.jsx(WithTooltip, { title: !hasPermission
|
|
1258
|
+
? t('You have no permission to share your audio')
|
|
1259
|
+
: !hasBrowserPermission
|
|
1260
|
+
? t('Check your browser mic permissions')
|
|
1261
|
+
: (caption ?? t('Mic')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
|
|
1262
|
+
!hasPermission ||
|
|
1263
|
+
// disable button while the toggle action is pending when not using optimistic updates
|
|
1264
|
+
(!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute ? 'audio-unmute-button' : 'audio-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
|
|
1265
|
+
setTooltipDisabled(shown);
|
|
1266
|
+
onMenuToggle?.(shown);
|
|
1267
|
+
}, children: [jsxRuntime.jsx(Icon, { icon: optionsAwareIsMute ? '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: "?" }))] }) }) }) }));
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
const ToggleVideoPreviewButton = (props) => {
|
|
1271
|
+
const { caption, Menu = DeviceSelectorVideo, menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
|
|
1272
|
+
const { t } = videoReactBindings.useI18n();
|
|
1273
|
+
const { useCameraState } = videoReactBindings.useCallStateHooks();
|
|
1274
|
+
const { camera, hasBrowserPermission, isPromptingPermission, isTogglePending, optionsAwareIsMute, } = useCameraState({ optimisticUpdates });
|
|
1275
|
+
const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
|
|
1276
|
+
const handleClick = createCallControlHandler(props, () => camera.toggle());
|
|
1277
|
+
return (jsxRuntime.jsx(WithTooltip, { title: !hasBrowserPermission
|
|
1278
|
+
? t('Check your browser video permissions')
|
|
1279
|
+
: (caption ?? t('Video')), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, className: clsx(!hasBrowserPermission && 'str-video__device-unavailable'), variant: "secondary", "data-testid": optionsAwareIsMute
|
|
1280
|
+
? 'preview-video-unmute-button'
|
|
1281
|
+
: 'preview-video-mute-button', onClick: handleClick, disabled: !hasBrowserPermission || (!optimisticUpdates && isTogglePending), Menu: Menu, menuPlacement: menuPlacement, ...restCompositeButtonProps, onMenuToggle: (shown) => {
|
|
1282
|
+
setTooltipDisabled(shown);
|
|
1283
|
+
onMenuToggle?.(shown);
|
|
1284
|
+
}, children: [jsxRuntime.jsx(Icon, { icon: !optionsAwareIsMute ? 'camera' : 'camera-off' }), !hasBrowserPermission && (jsxRuntime.jsx("span", { className: "str-video__no-media-permission", title: t('Check your browser video permissions'), children: "!" })), isPromptingPermission && (jsxRuntime.jsx("span", { className: "str-video__pending-permission", title: t('Waiting for permission'), children: "?" }))] }) }));
|
|
1285
|
+
};
|
|
1286
|
+
const ToggleVideoPublishingButton = (props) => {
|
|
1287
|
+
const { t } = videoReactBindings.useI18n();
|
|
1288
|
+
const { caption, Menu = jsxRuntime.jsx(DeviceSelectorVideo, { visualType: "list" }), menuPlacement = 'top', onMenuToggle, optimisticUpdates, ...restCompositeButtonProps } = props;
|
|
1289
|
+
const { hasPermission, requestPermission, isAwaitingPermission } = useRequestPermission(videoClient.OwnCapability.SEND_VIDEO);
|
|
1290
|
+
const { useCameraState, useCallSettings } = videoReactBindings.useCallStateHooks();
|
|
1291
|
+
const { camera, optionsAwareIsMute, hasBrowserPermission, isPromptingPermission, isTogglePending, } = useCameraState({ optimisticUpdates });
|
|
1292
|
+
const callSettings = useCallSettings();
|
|
1293
|
+
const isPublishingVideoAllowed = callSettings?.video.enabled;
|
|
1294
|
+
const [tooltipDisabled, setTooltipDisabled] = react.useState(false);
|
|
1295
|
+
const handleClick = createCallControlHandler(props, async () => {
|
|
1296
|
+
if (!hasPermission) {
|
|
1297
|
+
await requestPermission();
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
await camera.toggle();
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], children: jsxRuntime.jsx(PermissionNotification, { permission: videoClient.OwnCapability.SEND_VIDEO, isAwaitingApproval: isAwaitingPermission, messageApproved: t('You can now share your video.'), messageAwaitingApproval: t('Awaiting for an approval to share your video.'), messageRevoked: t('You can no longer share your video.'), children: jsxRuntime.jsx(WithTooltip, { title: !hasPermission
|
|
1304
|
+
? t('You have no permission to share your video')
|
|
1305
|
+
: !hasBrowserPermission
|
|
1306
|
+
? t('Check your browser video permissions')
|
|
1307
|
+
: !isPublishingVideoAllowed
|
|
1308
|
+
? t('Video publishing is disabled by the system')
|
|
1309
|
+
: caption || t('Video'), tooltipDisabled: tooltipDisabled, children: jsxRuntime.jsxs(CompositeButton, { active: optionsAwareIsMute, caption: caption, variant: "secondary", disabled: !hasBrowserPermission ||
|
|
1310
|
+
!hasPermission ||
|
|
1311
|
+
!isPublishingVideoAllowed ||
|
|
1312
|
+
(!optimisticUpdates && isTogglePending), "data-testid": optionsAwareIsMute ? 'video-unmute-button' : 'video-mute-button', onClick: handleClick, Menu: Menu, menuPlacement: menuPlacement, menuOffset: 16, ...restCompositeButtonProps, onMenuToggle: (shown) => {
|
|
1313
|
+
setTooltipDisabled(shown);
|
|
1314
|
+
onMenuToggle?.(shown);
|
|
1315
|
+
}, children: [jsxRuntime.jsx(Icon, { icon: optionsAwareIsMute ? 'camera-off' : 'camera' }), (!hasBrowserPermission ||
|
|
1316
|
+
!hasPermission ||
|
|
1317
|
+
!isPublishingVideoAllowed) && (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: "?" }))] }) }) }) }));
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
const EndCallMenu = (props) => {
|
|
1321
|
+
const { onLeave, onEnd } = props;
|
|
1322
|
+
const { t } = videoReactBindings.useI18n();
|
|
1323
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__end-call__confirmation", children: [jsxRuntime.jsxs("button", { className: "str-video__button str-video__end-call__leave", type: "button", "data-testid": "leave-call-button", onClick: onLeave, children: [jsxRuntime.jsx(Icon, { className: "str-video__button__icon str-video__end-call__leave-icon", icon: "logout" }), t('Leave call')] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.END_CALL], children: jsxRuntime.jsxs("button", { className: "str-video__button str-video__end-call__end", type: "button", "data-testid": "end-call-for-all-button", onClick: onEnd, children: [jsxRuntime.jsx(Icon, { className: "str-video__button__icon str-video__end-call__end-icon", icon: "call-end" }), t('End call for all')] }) })] }));
|
|
1324
|
+
};
|
|
1325
|
+
const CancelCallToggleMenuButton = react.forwardRef(function CancelCallToggleMenuButton({ menuShown }, ref) {
|
|
1326
|
+
const { t } = videoReactBindings.useI18n();
|
|
1327
|
+
return (jsxRuntime.jsx(WithTooltip, { title: t('Leave call'), tooltipDisabled: menuShown, children: jsxRuntime.jsx(IconButton, { icon: menuShown ? 'close' : 'call-end', variant: menuShown ? 'active' : 'danger', "data-testid": "leave-call-button", ref: ref }) }));
|
|
1328
|
+
});
|
|
1329
|
+
const CancelCallConfirmButton = ({ onClick, onLeave, }) => {
|
|
1330
|
+
const call = videoReactBindings.useCall();
|
|
1331
|
+
const handleLeave = react.useCallback(async (e) => {
|
|
1332
|
+
if (onClick) {
|
|
1333
|
+
onClick(e);
|
|
1334
|
+
}
|
|
1335
|
+
else if (call) {
|
|
1336
|
+
try {
|
|
1337
|
+
await call.leave();
|
|
1338
|
+
onLeave?.();
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
console.error(`Failed to leave call`, err);
|
|
1342
|
+
onLeave?.(err);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}, [onClick, onLeave, call]);
|
|
1346
|
+
const handleEndCall = react.useCallback(async (e) => {
|
|
1347
|
+
if (onClick) {
|
|
1348
|
+
onClick(e);
|
|
1349
|
+
}
|
|
1350
|
+
else if (call) {
|
|
1351
|
+
try {
|
|
1352
|
+
await call.endCall();
|
|
1353
|
+
onLeave?.();
|
|
1354
|
+
}
|
|
1355
|
+
catch (err) {
|
|
1356
|
+
console.error(`Failed to end call`, err);
|
|
1357
|
+
onLeave?.(err);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}, [onClick, onLeave, call]);
|
|
1361
|
+
return (jsxRuntime.jsx(MenuToggle, { placement: "top-start", ToggleButton: CancelCallToggleMenuButton, children: jsxRuntime.jsx(EndCallMenu, { onEnd: handleEndCall, onLeave: handleLeave }) }));
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
react.lazy(() => Promise.resolve().then(function () { return require('./embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js'); }));
|
|
1365
|
+
var Status;
|
|
1366
|
+
(function (Status) {
|
|
1367
|
+
Status["GOOD"] = "Good";
|
|
1368
|
+
Status["OK"] = "Ok";
|
|
1369
|
+
Status["BAD"] = "Bad";
|
|
1370
|
+
})(Status || (Status = {}));
|
|
1371
|
+
|
|
1372
|
+
react.forwardRef(function ToggleMenuButton(props, ref) {
|
|
1373
|
+
const { t } = videoReactBindings.useI18n();
|
|
1374
|
+
const { caption, menuShown } = props;
|
|
1375
|
+
return (jsxRuntime.jsx(CompositeButton, { ref: ref, active: menuShown, caption: caption, title: caption || t('Statistics'), "data-testid": "stats-button", children: jsxRuntime.jsx(Icon, { icon: "stats" }) }));
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
const BlockedUserListing = ({ data }) => {
|
|
1379
|
+
if (!data.length)
|
|
1380
|
+
return null;
|
|
1381
|
+
return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx("div", { className: "str-video__participant-listing", children: data.map((userId) => (jsxRuntime.jsx(BlockedUserListingItem, { userId: userId }, userId))) }) }));
|
|
1382
|
+
};
|
|
1383
|
+
const BlockedUserListingItem = ({ userId }) => {
|
|
1384
|
+
const call = videoReactBindings.useCall();
|
|
1385
|
+
const unblockUserClickHandler = () => {
|
|
1386
|
+
if (userId)
|
|
1387
|
+
call?.unblockUser(userId);
|
|
1388
|
+
};
|
|
1389
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item", children: [jsxRuntime.jsx("div", { className: "str-video__participant-listing-item__display-name", children: userId }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsx(TextButton, { onClick: unblockUserClickHandler, children: "Unblock" }) })] }));
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
const CallParticipantListHeader = ({ onClose, }) => {
|
|
1393
|
+
const { useParticipants, useAnonymousParticipantCount } = videoReactBindings.useCallStateHooks();
|
|
1394
|
+
const participants = useParticipants();
|
|
1395
|
+
const anonymousParticipantCount = useAnonymousParticipantCount();
|
|
1396
|
+
const { t } = videoReactBindings.useI18n();
|
|
1397
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__participant-list-header", children: [jsxRuntime.jsxs("div", { className: "str-video__participant-list-header__title", children: [t('Participants'), ' ', jsxRuntime.jsxs("span", { className: "str-video__participant-list-header__title-count", children: ["[", participants.length, "]"] }), anonymousParticipantCount > 0 && (jsxRuntime.jsx("span", { className: "str-video__participant-list-header__title-anonymous", children: t('Anonymous', { count: anonymousParticipantCount }) }))] }), jsxRuntime.jsx(IconButton, { onClick: onClose, className: "str-video__participant-list-header__close-button", icon: "close" })] }));
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
const CallParticipantListingItem = ({ participant, DisplayName = DefaultDisplayName, }) => {
|
|
1401
|
+
const isAudioOn = videoClient.hasAudio(participant);
|
|
1402
|
+
const isVideoOn = videoClient.hasVideo(participant);
|
|
1403
|
+
const isPinnedOn = videoClient.isPinned(participant);
|
|
1404
|
+
const { t } = videoReactBindings.useI18n();
|
|
1405
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item", children: [jsxRuntime.jsx(Avatar, { name: participant.name, imageSrc: participant.image }), jsxRuntime.jsx(DisplayName, { participant: participant }), jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item__media-indicator-group", children: [jsxRuntime.jsx(MediaIndicator, { title: isAudioOn ? t('Microphone on') : t('Microphone off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isAudioOn ? 'mic' : 'mic-off'}`) }), jsxRuntime.jsx(MediaIndicator, { title: isVideoOn ? t('Camera on') : t('Camera off'), className: clsx('str-video__participant-listing-item__icon', `str-video__participant-listing-item__icon-${isVideoOn ? 'camera' : 'camera-off'}`) }), isPinnedOn && (jsxRuntime.jsx(MediaIndicator, { title: t('Pinned'), className: clsx('str-video__participant-listing-item__icon', 'str-video__participant-listing-item__icon-pinned') })), jsxRuntime.jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$2, children: jsxRuntime.jsx(ParticipantViewContext.Provider, { value: { participant, trackType: 'none' }, children: jsxRuntime.jsx(ParticipantActionsContextMenu, {}) }) })] })] }));
|
|
1406
|
+
};
|
|
1407
|
+
const MediaIndicator = (props) => (jsxRuntime.jsx(WithTooltip, { ...props }));
|
|
1408
|
+
const DefaultDisplayName = ({ participant }) => {
|
|
1409
|
+
const connectedUser = videoReactBindings.useConnectedUser();
|
|
1410
|
+
const { t } = videoReactBindings.useI18n();
|
|
1411
|
+
const meFlag = participant.userId === connectedUser?.id ? t('Me') : '';
|
|
1412
|
+
const nameOrId = participant.name || participant.userId || t('Unknown');
|
|
1413
|
+
let displayName;
|
|
1414
|
+
if (!participant.name) {
|
|
1415
|
+
displayName = meFlag || nameOrId || t('Unknown');
|
|
1416
|
+
}
|
|
1417
|
+
else if (meFlag) {
|
|
1418
|
+
displayName = `${nameOrId} (${meFlag})`;
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
displayName = nameOrId;
|
|
1422
|
+
}
|
|
1423
|
+
return (jsxRuntime.jsx(WithTooltip, { className: "str-video__participant-listing-item__display-name", title: displayName, children: displayName }));
|
|
1424
|
+
};
|
|
1425
|
+
const ToggleButton$2 = react.forwardRef(function ToggleButton(props, ref) {
|
|
1426
|
+
return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
const CallParticipantListing = ({ data, }) => (jsxRuntime.jsx("div", { className: "str-video__participant-listing", children: data.map((participant) => (jsxRuntime.jsx(CallParticipantListingItem, { participant: participant }, participant.sessionId))) }));
|
|
1430
|
+
|
|
1431
|
+
const EmptyParticipantSearchList = () => {
|
|
1432
|
+
const { t } = videoReactBindings.useI18n();
|
|
1433
|
+
return (jsxRuntime.jsx("div", { className: "str-video__participant-list--empty", children: t('No participants found') }));
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
const SearchInput = ({ exitSearch, isActive, ...rest }) => {
|
|
1437
|
+
const [inputElement, setInputElement] = react.useState(null);
|
|
1438
|
+
react.useEffect(() => {
|
|
1439
|
+
if (!inputElement)
|
|
1440
|
+
return;
|
|
1441
|
+
const handleKeyDown = (e) => {
|
|
1442
|
+
if (e.key.toLowerCase() === 'escape')
|
|
1443
|
+
exitSearch();
|
|
1444
|
+
};
|
|
1445
|
+
inputElement.addEventListener('keydown', handleKeyDown);
|
|
1446
|
+
return () => {
|
|
1447
|
+
inputElement.removeEventListener('keydown', handleKeyDown);
|
|
1448
|
+
};
|
|
1449
|
+
}, [exitSearch, inputElement]);
|
|
1450
|
+
return (jsxRuntime.jsxs("div", { className: clsx('str-video__search-input__container', {
|
|
1451
|
+
'str-video__search-input__container--active': isActive,
|
|
1452
|
+
}), 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" }))] }));
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
function SearchResults({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) {
|
|
1456
|
+
if (searchQueryInProgress) {
|
|
1457
|
+
return (jsxRuntime.jsx("div", { className: "str-video__search-results--loading", children: jsxRuntime.jsx(LoadingIndicator$1, {}) }));
|
|
1458
|
+
}
|
|
1459
|
+
if (!searchResults.length) {
|
|
1460
|
+
return jsxRuntime.jsx(EmptySearchResultComponent, {});
|
|
1461
|
+
}
|
|
1462
|
+
return jsxRuntime.jsx(SearchResultList, { data: searchResults });
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const useSearch = ({ debounceInterval = 200, searchFn, searchQuery = '', }) => {
|
|
1466
|
+
const [searchResults, setSearchResults] = react.useState([]);
|
|
1467
|
+
const [searchQueryInProgress, setSearchQueryInProgress] = react.useState(false);
|
|
1468
|
+
const searchFnRef = react.useRef(searchFn);
|
|
1469
|
+
searchFnRef.current = searchFn;
|
|
1470
|
+
react.useEffect(() => {
|
|
1471
|
+
if (!searchQuery.length) {
|
|
1472
|
+
setSearchQueryInProgress(false);
|
|
1473
|
+
setSearchResults([]);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
setSearchQueryInProgress(true);
|
|
1477
|
+
const timeout = setTimeout(async () => {
|
|
1478
|
+
try {
|
|
1479
|
+
const results = await searchFnRef.current(searchQuery);
|
|
1480
|
+
setSearchResults(results);
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
console.error(error);
|
|
1484
|
+
}
|
|
1485
|
+
finally {
|
|
1486
|
+
setSearchQueryInProgress(false);
|
|
1487
|
+
}
|
|
1488
|
+
}, debounceInterval);
|
|
1489
|
+
return () => {
|
|
1490
|
+
clearTimeout(timeout);
|
|
1491
|
+
};
|
|
1492
|
+
}, [debounceInterval, searchQuery]);
|
|
1493
|
+
return {
|
|
1494
|
+
searchQueryInProgress,
|
|
1495
|
+
searchResults,
|
|
1496
|
+
};
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
const UserListTypes = {
|
|
1500
|
+
active: 'Active users',
|
|
1501
|
+
blocked: 'Blocked users',
|
|
1502
|
+
};
|
|
1503
|
+
const CallParticipantsList = ({ onClose, activeUsersSearchFn, blockedUsersSearchFn, debounceSearchInterval, }) => {
|
|
1504
|
+
const [searchQuery, setSearchQuery] = react.useState('');
|
|
1505
|
+
const [userListType, setUserListType] = react.useState('active');
|
|
1506
|
+
const exitSearch = react.useCallback(() => setSearchQuery(''), []);
|
|
1507
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__participant-list", children: [jsxRuntime.jsx(CallParticipantListHeader, { onClose: onClose }), jsxRuntime.jsx(SearchInput, { value: searchQuery, onChange: ({ currentTarget }) => setSearchQuery(currentTarget.value), exitSearch: exitSearch, isActive: !!searchQuery }), jsxRuntime.jsx(CallParticipantListContentHeader, { userListType: userListType, setUserListType: setUserListType }), jsxRuntime.jsxs("div", { className: "str-video__participant-list__content", children: [userListType === 'active' && (jsxRuntime.jsx(ActiveUsersSearchResults, { searchQuery: searchQuery, activeUsersSearchFn: activeUsersSearchFn, debounceSearchInterval: debounceSearchInterval })), userListType === 'blocked' && (jsxRuntime.jsx(BlockedUsersSearchResults, { searchQuery: searchQuery, blockedUsersSearchFn: blockedUsersSearchFn, debounceSearchInterval: debounceSearchInterval }))] })] }));
|
|
1508
|
+
};
|
|
1509
|
+
const CallParticipantListContentHeader = ({ userListType, setUserListType, }) => {
|
|
1510
|
+
const call = videoReactBindings.useCall();
|
|
1511
|
+
const { t } = videoReactBindings.useI18n();
|
|
1512
|
+
const muteAll = react.useCallback(() => {
|
|
1513
|
+
call?.muteAllUsers('audio');
|
|
1514
|
+
}, [call]);
|
|
1515
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__participant-list__content-header", children: [jsxRuntime.jsx("div", { className: "str-video__participant-list__content-header-title", children: userListType === 'active' && (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], hasPermissionsOnly: true, children: jsxRuntime.jsx(TextButton, { onClick: muteAll, children: t('Mute all') }) })) }), jsxRuntime.jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleButton$1, children: jsxRuntime.jsx(GenericMenu, { children: Object.keys(UserListTypes).map((lt) => (jsxRuntime.jsx(GenericMenuButtonItem, { "aria-selected": lt === userListType, onClick: () => setUserListType(lt), children: UserListTypes[lt] }, lt))) }) })] }));
|
|
1516
|
+
};
|
|
1517
|
+
const ActiveUsersSearchResults = ({ searchQuery, activeUsersSearchFn: activeUsersSearchFnFromProps, debounceSearchInterval, }) => {
|
|
1518
|
+
const { useParticipants } = videoReactBindings.useCallStateHooks();
|
|
1519
|
+
const participants = useParticipants({ sortBy: videoClient.name });
|
|
1520
|
+
const activeUsersSearchFn = react.useCallback(async (queryString) => {
|
|
1521
|
+
const normalizedQuery = normalizeString(queryString);
|
|
1522
|
+
const queryRegExp = new RegExp(normalizedQuery, 'i');
|
|
1523
|
+
return participants.filter((p) => normalizeString(p.name).match(queryRegExp));
|
|
1524
|
+
}, [participants]);
|
|
1525
|
+
const { searchQueryInProgress, searchResults } = useSearch({
|
|
1526
|
+
searchFn: activeUsersSearchFnFromProps ?? activeUsersSearchFn,
|
|
1527
|
+
debounceInterval: debounceSearchInterval,
|
|
1528
|
+
searchQuery,
|
|
1529
|
+
});
|
|
1530
|
+
return (jsxRuntime.jsx(SearchResults, { EmptySearchResultComponent: EmptyParticipantSearchList, LoadingIndicator: LoadingIndicator, searchQueryInProgress: searchQueryInProgress, searchResults: searchQuery ? searchResults : participants, SearchResultList: CallParticipantListing }));
|
|
1531
|
+
};
|
|
1532
|
+
const BlockedUsersSearchResults = ({ blockedUsersSearchFn: blockedUsersSearchFnFromProps, debounceSearchInterval, searchQuery, }) => {
|
|
1533
|
+
const { useCallBlockedUserIds } = videoReactBindings.useCallStateHooks();
|
|
1534
|
+
const blockedUsers = useCallBlockedUserIds();
|
|
1535
|
+
const blockedUsersSearchFn = react.useCallback(async (queryString) => {
|
|
1536
|
+
const queryRegExp = new RegExp(queryString, 'i');
|
|
1537
|
+
return blockedUsers.filter((userId) => userId.match(queryRegExp));
|
|
1538
|
+
}, [blockedUsers]);
|
|
1539
|
+
const { searchQueryInProgress, searchResults } = useSearch({
|
|
1540
|
+
searchFn: blockedUsersSearchFnFromProps ?? blockedUsersSearchFn,
|
|
1541
|
+
debounceInterval: debounceSearchInterval,
|
|
1542
|
+
searchQuery,
|
|
1543
|
+
});
|
|
1544
|
+
return (jsxRuntime.jsx(SearchResults, { EmptySearchResultComponent: EmptyParticipantSearchList, LoadingIndicator: LoadingIndicator, searchQueryInProgress: searchQueryInProgress, searchResults: searchQuery ? searchResults : blockedUsers, SearchResultList: BlockedUserListing }));
|
|
1545
|
+
};
|
|
1546
|
+
const ToggleButton$1 = react.forwardRef(function ToggleButton(props, ref) {
|
|
1547
|
+
return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "filter", ref: ref });
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
const NoiseCancellationContext = react.createContext(null);
|
|
1551
|
+
const NoiseCancellationProvider = (props) => {
|
|
1552
|
+
const { children, noiseCancellation } = props;
|
|
1553
|
+
const call = videoReactBindings.useCall();
|
|
1554
|
+
const { useCallSettings, useHasPermissions } = videoReactBindings.useCallStateHooks();
|
|
1555
|
+
const settings = useCallSettings();
|
|
1556
|
+
const noiseCancellationAllowed = !!(settings &&
|
|
1557
|
+
settings.audio.noise_cancellation &&
|
|
1558
|
+
settings.audio.noise_cancellation.mode !==
|
|
1559
|
+
videoClient.NoiseCancellationSettingsModeEnum.DISABLED);
|
|
1560
|
+
const hasCapability = useHasPermissions(videoClient.OwnCapability.ENABLE_NOISE_CANCELLATION);
|
|
1561
|
+
const [isSupportedByBrowser, setIsSupportedByBrowser] = react.useState();
|
|
1562
|
+
react.useEffect(() => {
|
|
1563
|
+
const result = noiseCancellation.isSupported();
|
|
1564
|
+
if (typeof result === 'boolean') {
|
|
1565
|
+
setIsSupportedByBrowser(result);
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
result
|
|
1569
|
+
.then((s) => setIsSupportedByBrowser(s))
|
|
1570
|
+
.catch((err) => console.error(`Can't determine if noise cancellation is supported`, err));
|
|
1571
|
+
}
|
|
1572
|
+
}, [noiseCancellation]);
|
|
1573
|
+
const isSupported = isSupportedByBrowser && hasCapability && noiseCancellationAllowed;
|
|
1574
|
+
const [isEnabled, setIsEnabled] = react.useState(false);
|
|
1575
|
+
const deinit = react.useRef(undefined);
|
|
1576
|
+
react.useEffect(() => {
|
|
1577
|
+
if (!call || !isSupported)
|
|
1578
|
+
return;
|
|
1579
|
+
noiseCancellation.isEnabled().then((e) => setIsEnabled(e));
|
|
1580
|
+
const unsubscribe = noiseCancellation.on('change', (v) => setIsEnabled(v));
|
|
1581
|
+
const init = (deinit.current || Promise.resolve())
|
|
1582
|
+
.then(() => noiseCancellation.init({ tracer: call.tracer }))
|
|
1583
|
+
.then(() => call.microphone.enableNoiseCancellation(noiseCancellation))
|
|
1584
|
+
.catch((e) => console.error(`Can't initialize noise cancellation`, e));
|
|
1585
|
+
return () => {
|
|
1586
|
+
deinit.current = init
|
|
1587
|
+
.then(() => call.microphone.disableNoiseCancellation())
|
|
1588
|
+
.then(() => noiseCancellation.dispose())
|
|
1589
|
+
.then(() => unsubscribe());
|
|
1590
|
+
};
|
|
1591
|
+
}, [call, isSupported, noiseCancellation]);
|
|
1592
|
+
const contextValue = react.useMemo(() => ({
|
|
1593
|
+
isSupported,
|
|
1594
|
+
isEnabled,
|
|
1595
|
+
setSuppressionLevel: (level) => {
|
|
1596
|
+
if (!noiseCancellation)
|
|
1597
|
+
return;
|
|
1598
|
+
noiseCancellation.setSuppressionLevel(level);
|
|
1599
|
+
},
|
|
1600
|
+
setEnabled: (enabledOrSetter) => {
|
|
1601
|
+
if (!noiseCancellation)
|
|
1602
|
+
return;
|
|
1603
|
+
const enable = typeof enabledOrSetter === 'function'
|
|
1604
|
+
? enabledOrSetter(isEnabled)
|
|
1605
|
+
: enabledOrSetter;
|
|
1606
|
+
if (enable) {
|
|
1607
|
+
noiseCancellation.enable().catch((err) => {
|
|
1608
|
+
console.error('Failed to enable noise cancellation', err);
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
noiseCancellation.disable().catch((err) => {
|
|
1613
|
+
console.error('Failed to disable noise cancellation', err);
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
}), [isEnabled, isSupported, noiseCancellation]);
|
|
1618
|
+
return (jsxRuntime.jsx(NoiseCancellationContext.Provider, { value: contextValue, children: children }));
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
({
|
|
1622
|
+
[videoClient.CallingState.JOINING]: 'Joining',
|
|
1623
|
+
[videoClient.CallingState.RINGING]: 'Ringing',
|
|
1624
|
+
[videoClient.CallingState.MIGRATING]: 'Migrating',
|
|
1625
|
+
[videoClient.CallingState.RECONNECTING]: 'Re-connecting',
|
|
1626
|
+
[videoClient.CallingState.RECONNECTING_FAILED]: 'Failed',
|
|
1627
|
+
[videoClient.CallingState.OFFLINE]: 'No internet connection',
|
|
1628
|
+
[videoClient.CallingState.IDLE]: '',
|
|
1629
|
+
[videoClient.CallingState.UNKNOWN]: '',
|
|
1630
|
+
[videoClient.CallingState.JOINED]: 'Joined',
|
|
1631
|
+
[videoClient.CallingState.LEFT]: 'Left call',
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
const byNameOrId = (a, b) => {
|
|
1635
|
+
if (a.name && b.name && a.name < b.name)
|
|
1636
|
+
return -1;
|
|
1637
|
+
if (a.name && b.name && a.name > b.name)
|
|
1638
|
+
return 1;
|
|
1639
|
+
if (a.id < b.id)
|
|
1640
|
+
return -1;
|
|
1641
|
+
if (a.id > b.id)
|
|
1642
|
+
return 1;
|
|
1643
|
+
return 0;
|
|
1644
|
+
};
|
|
1645
|
+
const PermissionRequests = () => {
|
|
1646
|
+
const call = videoReactBindings.useCall();
|
|
1647
|
+
const { useLocalParticipant, useHasPermissions } = videoReactBindings.useCallStateHooks();
|
|
1648
|
+
const localParticipant = useLocalParticipant();
|
|
1649
|
+
const [expanded, setExpanded] = react.useState(false);
|
|
1650
|
+
const [permissionRequests, setPermissionRequests] = react.useState([]);
|
|
1651
|
+
const canUpdateCallPermissions = useHasPermissions(videoClient.OwnCapability.UPDATE_CALL_PERMISSIONS);
|
|
1652
|
+
const localUserId = localParticipant?.userId;
|
|
1653
|
+
react.useEffect(() => {
|
|
1654
|
+
if (!call || !canUpdateCallPermissions)
|
|
1655
|
+
return;
|
|
1656
|
+
return call.on('call.permission_request', (event) => {
|
|
1657
|
+
if (event.user.id !== localUserId) {
|
|
1658
|
+
setPermissionRequests((requests) => [...requests, event].sort((a, b) => byNameOrId(a.user, b.user)));
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
}, [call, canUpdateCallPermissions, localUserId]);
|
|
1662
|
+
const handleUpdatePermission = (request, type) => {
|
|
1663
|
+
return async () => {
|
|
1664
|
+
const { user, permissions } = request;
|
|
1665
|
+
switch (type) {
|
|
1666
|
+
case 'grant':
|
|
1667
|
+
await call?.grantPermissions(user.id, permissions);
|
|
1668
|
+
break;
|
|
1669
|
+
case 'revoke':
|
|
1670
|
+
await call?.revokePermissions(user.id, permissions);
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
setPermissionRequests((requests) => requests.filter((r) => r !== request));
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
const { refs, x, y, strategy } = useFloatingUIPreset({
|
|
1677
|
+
placement: 'bottom',
|
|
1678
|
+
strategy: 'absolute',
|
|
1679
|
+
});
|
|
1680
|
+
// don't render anything if there are no permission requests
|
|
1681
|
+
if (permissionRequests.length === 0)
|
|
1682
|
+
return null;
|
|
1683
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__permission-requests", ref: refs.setReference, children: [jsxRuntime.jsxs("div", { className: "str-video__permission-requests__notification", children: [jsxRuntime.jsxs("span", { className: "str-video__permission-requests__notification__message", children: [permissionRequests.length, " pending permission requests"] }), jsxRuntime.jsx(Button, { type: "button", onClick: () => {
|
|
1684
|
+
setExpanded((e) => !e);
|
|
1685
|
+
}, children: expanded ? 'Hide requests' : 'Show requests' })] }), expanded && (jsxRuntime.jsx(PermissionRequestList, { ref: refs.setFloating, style: {
|
|
1686
|
+
position: strategy,
|
|
1687
|
+
top: y ?? 0,
|
|
1688
|
+
left: x ?? 0,
|
|
1689
|
+
overflowY: 'auto',
|
|
1690
|
+
}, permissionRequests: permissionRequests, handleUpdatePermission: handleUpdatePermission }))] }));
|
|
1691
|
+
};
|
|
1692
|
+
const PermissionRequestList = react.forwardRef(function PermissionRequestList(props, ref) {
|
|
1693
|
+
const { permissionRequests, handleUpdatePermission, ...rest } = props;
|
|
1694
|
+
const { t } = videoReactBindings.useI18n();
|
|
1695
|
+
return (jsxRuntime.jsx("div", { className: "str-video__permission-requests-list", ref: ref, ...rest, children: permissionRequests.map((request, reqIndex) => {
|
|
1696
|
+
const { user, permissions } = request;
|
|
1697
|
+
return (jsxRuntime.jsx(react.Fragment, { children: permissions.map((permission) => (jsxRuntime.jsxs("div", { className: "str-video__permission-request", children: [jsxRuntime.jsx("div", { className: "str-video__permission-request__message", children: messageForPermission(user.name || user.id, permission, t) }), jsxRuntime.jsx(Button, { className: "str-video__permission-request__button--allow", type: "button", onClick: handleUpdatePermission(request, 'grant'), children: t('Allow') }), jsxRuntime.jsx(Button, { className: "str-video__permission-request__button--reject", type: "button", onClick: handleUpdatePermission(request, 'revoke'), children: t('Revoke') }), jsxRuntime.jsx(Button, { className: "str-video__permission-request__button--reject", type: "button", onClick: handleUpdatePermission(request, 'dismiss'), children: t('Dismiss') })] }, permission))) }, `${user.id}/${reqIndex}`));
|
|
1698
|
+
}) }));
|
|
1699
|
+
});
|
|
1700
|
+
const Button = (props) => {
|
|
1701
|
+
const { className, ...rest } = props;
|
|
1702
|
+
return (jsxRuntime.jsx("button", { className: clsx('str-video__permission-request__button', className), ...rest }));
|
|
1703
|
+
};
|
|
1704
|
+
const messageForPermission = (userName, permission, t) => {
|
|
1705
|
+
switch (permission) {
|
|
1706
|
+
case videoClient.OwnCapability.SEND_AUDIO:
|
|
1707
|
+
return t('{{ userName }} is requesting to speak', { userName });
|
|
1708
|
+
case videoClient.OwnCapability.SEND_VIDEO:
|
|
1709
|
+
return t('{{ userName }} is requesting to share their camera', {
|
|
1710
|
+
userName,
|
|
1711
|
+
});
|
|
1712
|
+
case videoClient.OwnCapability.SCREENSHARE:
|
|
1713
|
+
return t('{{ userName }} is requesting to present their screen', {
|
|
1714
|
+
userName,
|
|
1715
|
+
});
|
|
1716
|
+
default:
|
|
1717
|
+
return t('{{ userName }} is requesting permission: {{ permission }}', {
|
|
1718
|
+
userName,
|
|
1719
|
+
permission,
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
const StreamTheme = ({ as: Component = 'div', className, children, ...props }) => {
|
|
1725
|
+
return (jsxRuntime.jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
const DefaultDisabledVideoPreview = () => {
|
|
1729
|
+
const { t } = videoReactBindings.useI18n();
|
|
1730
|
+
return (jsxRuntime.jsx("div", { className: "str_video__video-preview__disabled-video-preview", children: t('Video is disabled') }));
|
|
1731
|
+
};
|
|
1732
|
+
const DefaultNoCameraPreview = () => {
|
|
1733
|
+
const { t } = videoReactBindings.useI18n();
|
|
1734
|
+
return (jsxRuntime.jsx("div", { className: "str_video__video-preview__no-camera-preview", children: t('No camera found') }));
|
|
1735
|
+
};
|
|
1736
|
+
const VideoPreview = ({ className, mirror = true, DisabledVideoPreview = DefaultDisabledVideoPreview, NoCameraPreview = DefaultNoCameraPreview, StartingCameraPreview = LoadingIndicator, }) => {
|
|
1737
|
+
const { useCameraState } = videoReactBindings.useCallStateHooks();
|
|
1738
|
+
const { devices, status, isMute, mediaStream } = useCameraState();
|
|
1739
|
+
let contents;
|
|
1740
|
+
if (isMute && devices?.length === 0) {
|
|
1741
|
+
contents = jsxRuntime.jsx(NoCameraPreview, {});
|
|
1742
|
+
}
|
|
1743
|
+
else if (status === 'enabled') {
|
|
1744
|
+
const loading = !mediaStream;
|
|
1745
|
+
contents = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [mediaStream && (jsxRuntime.jsx(BaseVideo, { stream: mediaStream, className: clsx('str-video__video-preview', {
|
|
1746
|
+
'str-video__video-preview--mirror': mirror,
|
|
1747
|
+
'str-video__video-preview--loading': loading,
|
|
1748
|
+
}) })), loading && jsxRuntime.jsx(StartingCameraPreview, {})] }));
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
contents = jsxRuntime.jsx(DisabledVideoPreview, {});
|
|
1752
|
+
}
|
|
1753
|
+
return (jsxRuntime.jsx("div", { className: clsx('str-video__video-preview-container', className), children: contents }));
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
const ToggleButton = react.forwardRef(function ToggleButton(props, ref) {
|
|
1757
|
+
return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
|
|
1758
|
+
});
|
|
1759
|
+
const DefaultScreenShareOverlay = () => {
|
|
1760
|
+
const call = videoReactBindings.useCall();
|
|
1761
|
+
const { t } = videoReactBindings.useI18n();
|
|
1762
|
+
const stopScreenShare = () => {
|
|
1763
|
+
call?.screenShare.disable().catch((err) => {
|
|
1764
|
+
console.error('Failed to stop screen sharing:', err);
|
|
1765
|
+
});
|
|
1766
|
+
};
|
|
1767
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__screen-share-overlay", children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), jsxRuntime.jsx("span", { className: "str-video__screen-share-overlay__title", children: t('You are presenting your screen') }), jsxRuntime.jsxs("button", { onClick: stopScreenShare, type: "button", className: "str-video__screen-share-overlay__button", children: [jsxRuntime.jsx(Icon, { icon: "close" }), " ", t('Stop Screen Sharing')] })] }));
|
|
1768
|
+
};
|
|
1769
|
+
const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'bottom-start', showMenuButton = true, ParticipantActionsContextMenu: ParticipantActionsContextMenu$1 = ParticipantActionsContextMenu, }) => {
|
|
1770
|
+
const { participant, trackType } = useParticipantViewContext();
|
|
1771
|
+
const isScreenSharing = videoClient.hasScreenShare(participant);
|
|
1772
|
+
if (participant.isLocalParticipant &&
|
|
1773
|
+
isScreenSharing &&
|
|
1774
|
+
trackType === 'screenShareTrack') {
|
|
1775
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DefaultScreenShareOverlay, {}), jsxRuntime.jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
|
|
1776
|
+
}
|
|
1777
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [showMenuButton && (jsxRuntime.jsx(MenuToggle, { strategy: "fixed", placement: menuPlacement, ToggleButton: ToggleButton, children: jsxRuntime.jsx(ParticipantActionsContextMenu$1, {}) })), jsxRuntime.jsx(Reaction, { participant: participant }), jsxRuntime.jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
|
|
1778
|
+
};
|
|
1779
|
+
const ParticipantDetails = ({ indicatorsVisible = true, }) => {
|
|
1780
|
+
const { participant, trackType } = useParticipantViewContext();
|
|
1781
|
+
const { isLocalParticipant, connectionQuality, pin, sessionId, name, userId, } = participant;
|
|
1782
|
+
const call = videoReactBindings.useCall();
|
|
1783
|
+
const { t } = videoReactBindings.useI18n();
|
|
1784
|
+
const connectionQualityAsString = !!connectionQuality &&
|
|
1785
|
+
videoClient.SfuModels.ConnectionQuality[connectionQuality].toLowerCase();
|
|
1786
|
+
const hasAudioTrack = videoClient.hasAudio(participant);
|
|
1787
|
+
const hasVideoTrack = videoClient.hasVideo(participant);
|
|
1788
|
+
const canUnpin = !!pin && pin.isLocalPin;
|
|
1789
|
+
const isTrackPaused = trackType !== 'none' ? videoClient.hasPausedTrack(participant, trackType) : false;
|
|
1790
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "str-video__participant-details", children: jsxRuntime.jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && !hasAudioTrack && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideoTrack && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && isTrackPaused && (jsxRuntime.jsx("span", { title: t('Video paused due to insufficient bandwidth'), className: "str-video__participant-details__name--track-paused" })), indicatorsVisible && canUnpin && (
|
|
1791
|
+
// TODO: remove this monstrosity once we have a proper design
|
|
1792
|
+
jsxRuntime.jsx("span", { title: t('Unpin'), onClick: () => call?.unpin(sessionId), className: "str-video__participant-details__name--pinned" })), indicatorsVisible && jsxRuntime.jsx(SpeechIndicator, {})] }) }), indicatorsVisible && (jsxRuntime.jsx(Notification, { isVisible: isLocalParticipant &&
|
|
1793
|
+
connectionQuality === videoClient.SfuModels.ConnectionQuality.POOR, message: t('Poor connection quality'), children: connectionQualityAsString && (jsxRuntime.jsx("span", { className: clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) }))] }));
|
|
1794
|
+
};
|
|
1795
|
+
const SpeechIndicator = () => {
|
|
1796
|
+
const { participant } = useParticipantViewContext();
|
|
1797
|
+
const { isSpeaking, isDominantSpeaker } = participant;
|
|
1798
|
+
return (jsxRuntime.jsxs("span", { className: clsx('str-video__speech-indicator', isSpeaking && 'str-video__speech-indicator--speaking', isDominantSpeaker && 'str-video__speech-indicator--dominant'), children: [jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" }), jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" }), jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" })] }));
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
const ParticipantView = react.forwardRef(function ParticipantView({ participant, trackType = 'videoTrack', mirror, muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, PictureInPicturePlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) {
|
|
1802
|
+
const { isLocalParticipant, isSpeaking, isDominantSpeaker, sessionId } = participant;
|
|
1803
|
+
const hasAudioTrack = videoClient.hasAudio(participant);
|
|
1804
|
+
const hasVideoTrack = videoClient.hasVideo(participant);
|
|
1805
|
+
const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
|
|
1806
|
+
const [trackedElement, setTrackedElement] = react.useState(null);
|
|
1807
|
+
const [contextVideoElement, setContextVideoElement] = react.useState(null);
|
|
1808
|
+
const [contextVideoPlaceholderElement, setContextVideoPlaceholderElement] = react.useState(null);
|
|
1809
|
+
// TODO: allow to pass custom ViewportTracker instance from props
|
|
1810
|
+
useTrackElementVisibility({
|
|
1811
|
+
sessionId,
|
|
1812
|
+
trackedElement,
|
|
1813
|
+
trackType,
|
|
1814
|
+
});
|
|
1815
|
+
const { useIncomingVideoSettings } = videoReactBindings.useCallStateHooks();
|
|
1816
|
+
const { isParticipantVideoEnabled } = useIncomingVideoSettings();
|
|
1817
|
+
const participantViewContextValue = react.useMemo(() => ({
|
|
1818
|
+
participant,
|
|
1819
|
+
participantViewElement: trackedElement,
|
|
1820
|
+
videoElement: contextVideoElement,
|
|
1821
|
+
videoPlaceholderElement: contextVideoPlaceholderElement,
|
|
1822
|
+
trackType,
|
|
1823
|
+
}), [
|
|
1824
|
+
contextVideoElement,
|
|
1825
|
+
contextVideoPlaceholderElement,
|
|
1826
|
+
participant,
|
|
1827
|
+
trackedElement,
|
|
1828
|
+
trackType,
|
|
1829
|
+
]);
|
|
1830
|
+
const videoRefs = react.useMemo(() => ({
|
|
1831
|
+
setVideoElement: (element) => {
|
|
1832
|
+
setVideoElement?.(element);
|
|
1833
|
+
setContextVideoElement(element);
|
|
1834
|
+
},
|
|
1835
|
+
setVideoPlaceholderElement: (element) => {
|
|
1836
|
+
setVideoPlaceholderElement?.(element);
|
|
1837
|
+
setContextVideoPlaceholderElement(element);
|
|
1838
|
+
},
|
|
1839
|
+
}), [setVideoElement, setVideoPlaceholderElement]);
|
|
1840
|
+
return (jsxRuntime.jsx("div", { "data-testid": "participant-view", ref: (element) => {
|
|
1841
|
+
applyElementToRef(ref, element);
|
|
1842
|
+
setTrackedElement(element);
|
|
1843
|
+
}, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideoTrack && 'str-video__participant-view--no-video', !hasAudioTrack && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, PictureInPicturePlaceholder: PictureInPicturePlaceholder, participant: participant, trackType: trackType, refs: videoRefs, enabled: isLocalParticipant ||
|
|
1844
|
+
trackType !== 'videoTrack' ||
|
|
1845
|
+
isParticipantVideoEnabled(participant.sessionId), mirror: mirror, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
|
|
1846
|
+
});
|
|
1847
|
+
ParticipantView.displayName = 'ParticipantView';
|
|
1848
|
+
|
|
1849
|
+
// re-exporting the StreamCallProvider as StreamCall
|
|
1850
|
+
const StreamCall = videoReactBindings.StreamCallProvider;
|
|
1851
|
+
StreamCall.displayName = 'StreamCall';
|
|
1852
|
+
|
|
1853
|
+
var Joining = "Joining";
|
|
1854
|
+
var Mic = "Mic";
|
|
1855
|
+
var Ringing = "Ringing";
|
|
1856
|
+
var Speakers = "Speakers";
|
|
1857
|
+
var Video = "Video";
|
|
1858
|
+
var Live = "Live";
|
|
1859
|
+
var Reactions = "Reactions";
|
|
1860
|
+
var Statistics = "Statistics";
|
|
1861
|
+
var Invite = "Invite";
|
|
1862
|
+
var Join = "Join";
|
|
1863
|
+
var You = "You";
|
|
1864
|
+
var Me = "Me";
|
|
1865
|
+
var Unknown = "Unknown";
|
|
1866
|
+
var Default = "Default";
|
|
1867
|
+
var Refresh = "Refresh";
|
|
1868
|
+
var Allow = "Allow";
|
|
1869
|
+
var Revoke = "Revoke";
|
|
1870
|
+
var Dismiss = "Dismiss";
|
|
1871
|
+
var Pinned = "Pinned";
|
|
1872
|
+
var Unpin = "Unpin";
|
|
1873
|
+
var Pin = "Pin";
|
|
1874
|
+
var Block = "Block";
|
|
1875
|
+
var Kick = "Kick";
|
|
1876
|
+
var Enter = "Enter";
|
|
1877
|
+
var Leave = "Leave";
|
|
1878
|
+
var Participants = "Participants";
|
|
1879
|
+
var Anonymous = ", and ({{ count }}) anonymous";
|
|
1880
|
+
var Speaker = "Speaker";
|
|
1881
|
+
var Microphone = "Microphone";
|
|
1882
|
+
var Backstage = "Backstage";
|
|
1883
|
+
var en = {
|
|
1884
|
+
Joining: Joining,
|
|
1885
|
+
Mic: Mic,
|
|
1886
|
+
"No internet connection": "No internet connection",
|
|
1887
|
+
"Re-connecting": "Re-connecting",
|
|
1888
|
+
Ringing: Ringing,
|
|
1889
|
+
"Screen Share": "Screen Share",
|
|
1890
|
+
"Select a Camera": "Select a Camera",
|
|
1891
|
+
"Select a Mic": "Select a Mic",
|
|
1892
|
+
"Select Speakers": "Select Speakers",
|
|
1893
|
+
Speakers: Speakers,
|
|
1894
|
+
Video: Video,
|
|
1895
|
+
"You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
|
|
1896
|
+
"Background filters performance is degraded. Consider disabling filters for better performance.": "Background filters performance is degraded. Consider disabling filters for better performance.",
|
|
1897
|
+
Live: Live,
|
|
1898
|
+
"Livestream starts soon": "Livestream starts soon",
|
|
1899
|
+
"Livestream starts at {{ time }}": "Livestream starts at {{ time }}",
|
|
1900
|
+
"{{ count }} participants joined early_one": "{{ count }} participant joined early",
|
|
1901
|
+
"{{ count }} participants joined early_other": "{{ count }} participants joined early",
|
|
1902
|
+
"You can now speak.": "You can now speak.",
|
|
1903
|
+
"Awaiting for an approval to speak.": "Awaiting for an approval to speak.",
|
|
1904
|
+
"You can no longer speak.": "You can no longer speak.",
|
|
1905
|
+
"You can now share your video.": "You can now share your video.",
|
|
1906
|
+
"Awaiting for an approval to share your video.": "Awaiting for an approval to share your video.",
|
|
1907
|
+
"You can no longer share your video.": "You can no longer share your video.",
|
|
1908
|
+
"Waiting for recording to stop...": "Waiting for recording to stop...",
|
|
1909
|
+
"Waiting for recording to start...": "Waiting for recording to start...",
|
|
1910
|
+
"Record call": "Record call",
|
|
1911
|
+
Reactions: Reactions,
|
|
1912
|
+
Statistics: Statistics,
|
|
1913
|
+
"You can now share your screen.": "You can now share your screen.",
|
|
1914
|
+
"Awaiting for an approval to share screen.": "Awaiting for an approval to share screen.",
|
|
1915
|
+
"You can no longer share your screen.": "You can no longer share your screen.",
|
|
1916
|
+
"Share screen": "Share screen",
|
|
1917
|
+
"Incoming Call...": "Incoming Call...",
|
|
1918
|
+
"Calling...": "Calling...",
|
|
1919
|
+
"Mute All": "Mute All",
|
|
1920
|
+
Invite: Invite,
|
|
1921
|
+
Join: Join,
|
|
1922
|
+
You: You,
|
|
1923
|
+
Me: Me,
|
|
1924
|
+
Unknown: Unknown,
|
|
1925
|
+
"Toggle device menu": "Toggle device menu",
|
|
1926
|
+
Default: Default,
|
|
1927
|
+
"Call Recordings": "Call Recordings",
|
|
1928
|
+
Refresh: Refresh,
|
|
1929
|
+
"Check your browser video permissions": "Check your browser video permissions",
|
|
1930
|
+
"Video publishing is disabled by the system": "Video publishing is disabled by the system",
|
|
1931
|
+
"You have no permission to share your video": "You have no permission to share your video",
|
|
1932
|
+
"You have no permission to share your audio": "You have no permission to share your audio",
|
|
1933
|
+
"You are presenting your screen": "You are presenting your screen",
|
|
1934
|
+
"Stop Screen Sharing": "Stop Screen Sharing",
|
|
1935
|
+
Allow: Allow,
|
|
1936
|
+
Revoke: Revoke,
|
|
1937
|
+
Dismiss: Dismiss,
|
|
1938
|
+
"Microphone on": "Microphone on",
|
|
1939
|
+
"Microphone off": "Microphone off",
|
|
1940
|
+
"Camera on": "Camera on",
|
|
1941
|
+
"Camera off": "Camera off",
|
|
1942
|
+
"No camera found": "No camera found",
|
|
1943
|
+
"Video is disabled": "Video is disabled",
|
|
1944
|
+
Pinned: Pinned,
|
|
1945
|
+
Unpin: Unpin,
|
|
1946
|
+
Pin: Pin,
|
|
1947
|
+
"Pin for everyone": "Pin for everyone",
|
|
1948
|
+
"Unpin for everyone": "Unpin for everyone",
|
|
1949
|
+
Block: Block,
|
|
1950
|
+
Kick: Kick,
|
|
1951
|
+
"Turn off video": "Turn off video",
|
|
1952
|
+
"Turn off screen share": "Turn off screen share",
|
|
1953
|
+
"Mute audio": "Mute audio",
|
|
1954
|
+
"Mute screen share audio": "Mute screen share audio",
|
|
1955
|
+
"Allow audio": "Allow audio",
|
|
1956
|
+
"Allow video": "Allow video",
|
|
1957
|
+
"Allow screen sharing": "Allow screen sharing",
|
|
1958
|
+
"Disable audio": "Disable audio",
|
|
1959
|
+
"Disable video": "Disable video",
|
|
1960
|
+
"Disable screen sharing": "Disable screen sharing",
|
|
1961
|
+
Enter: Enter,
|
|
1962
|
+
Leave: Leave,
|
|
1963
|
+
"Leave call": "Leave call",
|
|
1964
|
+
"End call for all": "End call for all",
|
|
1965
|
+
"{{ direction }} fullscreen": "{{ direction }} fullscreen",
|
|
1966
|
+
"{{ direction }} picture-in-picture": "{{ direction }} picture-in-picture",
|
|
1967
|
+
"Dominant Speaker": "Dominant Speaker",
|
|
1968
|
+
"Poor connection quality": "Poor connection quality. Please check your internet connection.",
|
|
1969
|
+
"Video paused due to insufficient bandwidth": "Video paused due to insufficient bandwidth",
|
|
1970
|
+
Participants: Participants,
|
|
1971
|
+
Anonymous: Anonymous,
|
|
1972
|
+
"No participants found": "No participants found",
|
|
1973
|
+
"Participants ({{ numberOfParticipants }})": "Participants ({{ numberOfParticipants }})",
|
|
1974
|
+
"{{ userName }} is sharing their screen": "{{ userName }} is sharing their screen",
|
|
1975
|
+
"{{ userName }} is requesting to speak": "{{ userName }} is requesting to speak",
|
|
1976
|
+
"{{ userName }} is requesting to share their camera": "{{ userName }} is requesting to share their camera",
|
|
1977
|
+
"{{ userName }} is requesting to present their screen": "{{ userName }} is requesting to present their screen",
|
|
1978
|
+
"{{ userName }} is requesting permission: {{ permission }}": "{{ userName }} is requesting permission: {{ permission }}",
|
|
1979
|
+
"Applying...": "Applying...",
|
|
1980
|
+
"Disable blur": "Disable blur",
|
|
1981
|
+
"Blur background": "Blur background",
|
|
1982
|
+
"Migrating...": "Migrating...",
|
|
1983
|
+
"Reconnecting...": "Reconnecting...",
|
|
1984
|
+
"You are offline. Check your internet connection.": "You are offline. Check your internet connection.",
|
|
1985
|
+
"Failed to restore connection. Please try again.": "Failed to restore connection. Please try again.",
|
|
1986
|
+
"Failed to join. Please try again.": "Failed to join. Please try again.",
|
|
1987
|
+
"Set up your call before joining": "Set up your call before joining",
|
|
1988
|
+
"Please grant your browser permission to access your camera and microphone.": "Please grant your browser permission to access your camera and microphone.",
|
|
1989
|
+
"Start call": "Start call",
|
|
1990
|
+
Speaker: Speaker,
|
|
1991
|
+
Microphone: Microphone,
|
|
1992
|
+
Backstage: Backstage,
|
|
1993
|
+
"Go Live": "Go Live",
|
|
1994
|
+
"Stop Live": "End Live",
|
|
1995
|
+
"Enter Backstage": "Enter Backstage",
|
|
1996
|
+
"Prepare your livestream": "Prepare your livestream",
|
|
1997
|
+
"Ready to go live": "Ready to go live",
|
|
1998
|
+
"Stream is ready!": "Stream is ready!",
|
|
1999
|
+
"Waiting for the livestream to start": "Waiting for the livestream to start",
|
|
2000
|
+
"{{ count }} waiting": "{{ count }} waiting",
|
|
2001
|
+
"Join Stream": "Join Stream",
|
|
2002
|
+
"Join automatically when stream starts": "Join automatically when stream starts",
|
|
2003
|
+
"Display name": "Display name",
|
|
2004
|
+
"Permission needed": "Permission needed",
|
|
2005
|
+
"Call ended": "Call ended",
|
|
2006
|
+
"Rejoin call": "Rejoin call",
|
|
2007
|
+
"Left by mistake?": "Left by mistake?",
|
|
2008
|
+
"Help us improve": "Help us improve",
|
|
2009
|
+
"Leave feedback": "Leave feedback",
|
|
2010
|
+
"Failed to rejoin. Please try again.": "Failed to rejoin. Please try again.",
|
|
2011
|
+
"Share your feedback": "Share your feedback",
|
|
2012
|
+
"Tell us about your experience...": "Tell us about your experience...",
|
|
2013
|
+
"Submit feedback": "Submit feedback",
|
|
2014
|
+
"Feedback message": "Feedback message",
|
|
2015
|
+
"How was your call quality?": "How was your call quality?",
|
|
2016
|
+
"Rate {{ count }} star_one": "Rate {{ count }} star",
|
|
2017
|
+
"Rate {{ count }} star_other": "Rate {{ count }} stars",
|
|
2018
|
+
"Thank you!": "Thank you!",
|
|
2019
|
+
"Your feedback helps improve call quality.": "Your feedback helps improve call quality."
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
const translations = { en };
|
|
2023
|
+
|
|
2024
|
+
const StreamVideo = (props) => {
|
|
2025
|
+
return (jsxRuntime.jsx(videoReactBindings.StreamVideoProvider, { translationsOverrides: translations, ...props }));
|
|
2026
|
+
};
|
|
2027
|
+
StreamVideo.displayName = 'StreamVideo';
|
|
2028
|
+
|
|
2029
|
+
function applyFilter(obj, filter) {
|
|
2030
|
+
if ('$and' in filter) {
|
|
2031
|
+
return filter.$and.every((f) => applyFilter(obj, f));
|
|
2032
|
+
}
|
|
2033
|
+
if ('$or' in filter) {
|
|
2034
|
+
return filter.$or.some((f) => applyFilter(obj, f));
|
|
2035
|
+
}
|
|
2036
|
+
if ('$not' in filter) {
|
|
2037
|
+
return !applyFilter(obj, filter.$not);
|
|
2038
|
+
}
|
|
2039
|
+
return checkConditions(obj, filter);
|
|
2040
|
+
}
|
|
2041
|
+
const isDateString = (value) => typeof value === 'string' &&
|
|
2042
|
+
/^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[+-]\d{2}:\d{2})?)$/.test(value);
|
|
2043
|
+
function checkConditions(obj, conditions) {
|
|
2044
|
+
let match = true;
|
|
2045
|
+
for (const key of Object.keys(conditions)) {
|
|
2046
|
+
const operator = conditions[key];
|
|
2047
|
+
const maybeOperator = operator && typeof operator === 'object';
|
|
2048
|
+
let value = obj[key];
|
|
2049
|
+
if (value instanceof Date) {
|
|
2050
|
+
value = value.getTime();
|
|
2051
|
+
}
|
|
2052
|
+
else if (isDateString(value)) {
|
|
2053
|
+
value = new Date(value).getTime();
|
|
2054
|
+
}
|
|
2055
|
+
if (maybeOperator && '$eq' in operator) {
|
|
2056
|
+
const eqOperator = operator;
|
|
2057
|
+
const eqOperatorValue = isDateString(eqOperator.$eq)
|
|
2058
|
+
? new Date(eqOperator.$eq).getTime()
|
|
2059
|
+
: eqOperator.$eq;
|
|
2060
|
+
match && (match = eqOperatorValue === value);
|
|
2061
|
+
}
|
|
2062
|
+
else if (maybeOperator && '$neq' in operator) {
|
|
2063
|
+
const neqOperator = operator;
|
|
2064
|
+
match && (match = neqOperator.$neq !== value);
|
|
2065
|
+
}
|
|
2066
|
+
else if (maybeOperator && '$in' in operator) {
|
|
2067
|
+
const inOperator = operator;
|
|
2068
|
+
match && (match = inOperator.$in.includes(value));
|
|
2069
|
+
}
|
|
2070
|
+
else if (maybeOperator && '$contains' in operator) {
|
|
2071
|
+
if (Array.isArray(value)) {
|
|
2072
|
+
const containsOperator = operator;
|
|
2073
|
+
match && (match = value.includes(containsOperator.$contains));
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
match = false;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
else if (maybeOperator && '$gt' in operator) {
|
|
2080
|
+
const gtOperator = operator;
|
|
2081
|
+
const gtOperatorValue = isDateString(gtOperator.$gt)
|
|
2082
|
+
? new Date(gtOperator.$gt).getTime()
|
|
2083
|
+
: gtOperator.$gt;
|
|
2084
|
+
match && (match = value > gtOperatorValue);
|
|
2085
|
+
}
|
|
2086
|
+
else if (maybeOperator && '$gte' in operator) {
|
|
2087
|
+
const gteOperator = operator;
|
|
2088
|
+
const gteOperatorValue = isDateString(gteOperator.$gte)
|
|
2089
|
+
? new Date(gteOperator.$gte).getTime()
|
|
2090
|
+
: gteOperator.$gte;
|
|
2091
|
+
match && (match = value >= gteOperatorValue);
|
|
2092
|
+
}
|
|
2093
|
+
else if (maybeOperator && '$lt' in operator) {
|
|
2094
|
+
const ltOperator = operator;
|
|
2095
|
+
const ltOperatorValue = isDateString(ltOperator.$lt)
|
|
2096
|
+
? new Date(ltOperator.$lt).getTime()
|
|
2097
|
+
: ltOperator.$lt;
|
|
2098
|
+
match && (match = value < ltOperatorValue);
|
|
2099
|
+
}
|
|
2100
|
+
else if (maybeOperator && '$lte' in operator) {
|
|
2101
|
+
const lteOperator = operator;
|
|
2102
|
+
const lteOperatorValue = isDateString(lteOperator.$lte)
|
|
2103
|
+
? new Date(lteOperator.$lte).getTime()
|
|
2104
|
+
: lteOperator.$lte;
|
|
2105
|
+
match && (match = value <= lteOperatorValue);
|
|
2106
|
+
// } else if (maybeOperator && '$autocomplete' in operator) {
|
|
2107
|
+
// TODO: regexp solution maybe?
|
|
2108
|
+
// match &&= false;
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
const eqValue = operator;
|
|
2112
|
+
match && (match = eqValue === value);
|
|
2113
|
+
}
|
|
2114
|
+
if (!match) {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const useFilteredParticipants = ({ excludeLocalParticipant = false, filterParticipants, }) => {
|
|
2122
|
+
const { useParticipants, useRemoteParticipants } = videoReactBindings.useCallStateHooks();
|
|
2123
|
+
const allParticipants = useParticipants();
|
|
2124
|
+
const remoteParticipants = useRemoteParticipants();
|
|
2125
|
+
return react.useMemo(() => {
|
|
2126
|
+
const unfilteredParticipants = excludeLocalParticipant
|
|
2127
|
+
? remoteParticipants
|
|
2128
|
+
: allParticipants;
|
|
2129
|
+
return filterParticipants
|
|
2130
|
+
? applyParticipantsFilter(unfilteredParticipants, filterParticipants)
|
|
2131
|
+
: unfilteredParticipants;
|
|
2132
|
+
}, [
|
|
2133
|
+
allParticipants,
|
|
2134
|
+
remoteParticipants,
|
|
2135
|
+
excludeLocalParticipant,
|
|
2136
|
+
filterParticipants,
|
|
2137
|
+
]);
|
|
2138
|
+
};
|
|
2139
|
+
const applyParticipantsFilter = (participants, filter) => {
|
|
2140
|
+
const filterCallback = typeof filter === 'function'
|
|
2141
|
+
? filter
|
|
2142
|
+
: (participant) => applyFilter({
|
|
2143
|
+
userId: participant.userId,
|
|
2144
|
+
isSpeaking: participant.isSpeaking,
|
|
2145
|
+
isDominantSpeaker: participant.isDominantSpeaker,
|
|
2146
|
+
name: participant.name,
|
|
2147
|
+
roles: participant.roles,
|
|
2148
|
+
isPinned: videoClient.isPinned(participant),
|
|
2149
|
+
hasVideo: videoClient.hasVideo(participant),
|
|
2150
|
+
hasAudio: videoClient.hasAudio(participant),
|
|
2151
|
+
hasScreenShare: videoClient.hasScreenShare(participant),
|
|
2152
|
+
}, filter);
|
|
2153
|
+
return participants.filter(filterCallback);
|
|
2154
|
+
};
|
|
2155
|
+
const usePaginatedLayoutSortPreset = (call) => {
|
|
2156
|
+
react.useEffect(() => {
|
|
2157
|
+
if (!call)
|
|
2158
|
+
return;
|
|
2159
|
+
call.setSortParticipantsBy(videoClient.paginatedLayoutSortPreset);
|
|
2160
|
+
return () => {
|
|
2161
|
+
resetSortPreset(call);
|
|
2162
|
+
};
|
|
2163
|
+
}, [call]);
|
|
2164
|
+
};
|
|
2165
|
+
const useSpeakerLayoutSortPreset = (call, isOneOnOneCall) => {
|
|
2166
|
+
react.useEffect(() => {
|
|
2167
|
+
if (!call)
|
|
2168
|
+
return;
|
|
2169
|
+
// always show the remote participant in the spotlight
|
|
2170
|
+
if (isOneOnOneCall) {
|
|
2171
|
+
call.setSortParticipantsBy(videoClient.combineComparators(videoClient.screenSharing, loggedIn));
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
call.setSortParticipantsBy(videoClient.speakerLayoutSortPreset);
|
|
2175
|
+
}
|
|
2176
|
+
return () => {
|
|
2177
|
+
resetSortPreset(call);
|
|
2178
|
+
};
|
|
2179
|
+
}, [call, isOneOnOneCall]);
|
|
2180
|
+
};
|
|
2181
|
+
const useRawRemoteParticipants = () => {
|
|
2182
|
+
const { useRawParticipants } = videoReactBindings.useCallStateHooks();
|
|
2183
|
+
const rawParticipants = useRawParticipants();
|
|
2184
|
+
return react.useMemo(() => rawParticipants.filter((p) => !p.isLocalParticipant), [rawParticipants]);
|
|
2185
|
+
};
|
|
2186
|
+
const resetSortPreset = (call) => {
|
|
2187
|
+
// reset the sorting to the default for the call type
|
|
2188
|
+
const callConfig = videoClient.CallTypes.get(call.type);
|
|
2189
|
+
call.setSortParticipantsBy(callConfig.options.sortParticipantsBy || videoClient.defaultSortPreset);
|
|
2190
|
+
};
|
|
2191
|
+
const loggedIn = (a, b) => {
|
|
2192
|
+
if (a.isLocalParticipant)
|
|
2193
|
+
return 1;
|
|
2194
|
+
if (b.isLocalParticipant)
|
|
2195
|
+
return -1;
|
|
2196
|
+
return 0;
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
const LivestreamLayout = (props) => {
|
|
2200
|
+
const { useParticipants, useHasOngoingScreenShare } = videoReactBindings.useCallStateHooks();
|
|
2201
|
+
const call = videoReactBindings.useCall();
|
|
2202
|
+
const participants = useParticipants();
|
|
2203
|
+
const [currentSpeaker] = participants;
|
|
2204
|
+
const remoteParticipants = useRawRemoteParticipants();
|
|
2205
|
+
const hasOngoingScreenShare = useHasOngoingScreenShare();
|
|
2206
|
+
const presenter = hasOngoingScreenShare
|
|
2207
|
+
? participants.find(videoClient.hasScreenShare)
|
|
2208
|
+
: undefined;
|
|
2209
|
+
usePaginatedLayoutSortPreset(call);
|
|
2210
|
+
const { floatingParticipantProps, muted, ParticipantViewUI } = props;
|
|
2211
|
+
const overlay = ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showMuteButton: props.showMuteButton, showSpeakerName: props.showSpeakerName, enableFullScreen: props.enableFullScreen }));
|
|
2212
|
+
const floatingParticipantOverlay = hasOngoingScreenShare &&
|
|
2213
|
+
(ParticipantViewUI ?? (jsxRuntime.jsx(ParticipantOverlay
|
|
2214
|
+
// these elements aren't needed for the video feed
|
|
2215
|
+
, {
|
|
2216
|
+
// these elements aren't needed for the video feed
|
|
2217
|
+
showParticipantCount: floatingParticipantProps?.showParticipantCount ?? false, showDuration: floatingParticipantProps?.showDuration ?? false, showLiveBadge: floatingParticipantProps?.showLiveBadge ?? false, showSpeakerName: floatingParticipantProps?.showSpeakerName ?? true, enableFullScreen: floatingParticipantProps?.enableFullScreen ?? true })));
|
|
2218
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), hasOngoingScreenShare && presenter && (jsxRuntime.jsx(ParticipantView, { className: "str-video__livestream-layout__screen-share", participant: presenter, ParticipantViewUI: overlay, trackType: "screenShareTrack", muteAudio // audio is rendered by ParticipantsAudio
|
|
2219
|
+
: true })), currentSpeaker && (jsxRuntime.jsx(ParticipantView, { className: clsx(hasOngoingScreenShare &&
|
|
2220
|
+
clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: floatingParticipantOverlay || overlay, mirror: props.mirrorLocalParticipantVideo !== false ? undefined : false, muteAudio // audio is rendered by ParticipantsAudio
|
|
2221
|
+
: true }))] }));
|
|
2222
|
+
};
|
|
2223
|
+
LivestreamLayout.displayName = 'LivestreamLayout';
|
|
2224
|
+
const ParticipantOverlay = (props) => {
|
|
2225
|
+
const { enableFullScreen = true, showParticipantCount = true, humanizeParticipantCount = true, showDuration = true, showLiveBadge = true, showMuteButton = true, showSpeakerName = false, } = props;
|
|
2226
|
+
const overlayBarVisible = enableFullScreen ||
|
|
2227
|
+
showParticipantCount ||
|
|
2228
|
+
showDuration ||
|
|
2229
|
+
showLiveBadge ||
|
|
2230
|
+
showMuteButton ||
|
|
2231
|
+
showSpeakerName;
|
|
2232
|
+
const { participant, participantViewElement } = useParticipantViewContext();
|
|
2233
|
+
const { useParticipantCount, useSpeakerState } = videoReactBindings.useCallStateHooks();
|
|
2234
|
+
const participantCount = useParticipantCount();
|
|
2235
|
+
const duration = useUpdateCallDuration();
|
|
2236
|
+
const toggleFullScreen = useToggleFullScreen();
|
|
2237
|
+
const { speaker, volume } = useSpeakerState();
|
|
2238
|
+
const isSpeakerMuted = volume === 0;
|
|
2239
|
+
const { t } = videoReactBindings.useI18n();
|
|
2240
|
+
return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: overlayBarVisible && (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar", children: [jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar-left", children: [showLiveBadge && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__live-badge", children: t('Live') })), showParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__viewers-count", children: humanizeParticipantCount
|
|
2241
|
+
? videoClient.humanize(participantCount)
|
|
2242
|
+
: participantCount })), showSpeakerName && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__speaker-name", title: participant.name || participant.userId || '', children: participant.name || participant.userId || '' }))] }), jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay__bar-center", children: showDuration && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__duration", children: formatDuration(duration) })) }), jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar-right", children: [showMuteButton && (jsxRuntime.jsx("span", { className: clsx('str-video__livestream-layout__mute-button', isSpeakerMuted &&
|
|
2243
|
+
'str-video__livestream-layout__mute-button--muted'), onClick: () => speaker.setVolume(isSpeakerMuted ? 1 : 0) })), enableFullScreen &&
|
|
2244
|
+
participantViewElement &&
|
|
2245
|
+
typeof participantViewElement.requestFullscreen !==
|
|
2246
|
+
'undefined' && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__go-fullscreen", onClick: toggleFullScreen }))] })] })) }));
|
|
2247
|
+
};
|
|
2248
|
+
const useUpdateCallDuration = () => {
|
|
2249
|
+
const { useIsCallLive, useCallSession } = videoReactBindings.useCallStateHooks();
|
|
2250
|
+
const isCallLive = useIsCallLive();
|
|
2251
|
+
const session = useCallSession();
|
|
2252
|
+
const [duration, setDuration] = react.useState(() => {
|
|
2253
|
+
if (!session || !session.live_started_at)
|
|
2254
|
+
return 0;
|
|
2255
|
+
const liveStartTime = new Date(session.live_started_at);
|
|
2256
|
+
const now = new Date();
|
|
2257
|
+
return Math.floor((now.getTime() - liveStartTime.getTime()) / 1000);
|
|
2258
|
+
});
|
|
2259
|
+
react.useEffect(() => {
|
|
2260
|
+
if (!isCallLive)
|
|
2261
|
+
return;
|
|
2262
|
+
const interval = setInterval(() => {
|
|
2263
|
+
setDuration((d) => d + 1);
|
|
2264
|
+
}, 1000);
|
|
2265
|
+
return () => {
|
|
2266
|
+
clearInterval(interval);
|
|
2267
|
+
};
|
|
2268
|
+
}, [isCallLive]);
|
|
2269
|
+
return duration;
|
|
2270
|
+
};
|
|
2271
|
+
const useToggleFullScreen = () => {
|
|
2272
|
+
const { participantViewElement } = useParticipantViewContext();
|
|
2273
|
+
const [isFullscreen, setIsFullscreen] = react.useState(!!document.fullscreenElement);
|
|
2274
|
+
react.useEffect(() => {
|
|
2275
|
+
const handler = () => setIsFullscreen(!!document.fullscreenElement);
|
|
2276
|
+
document.addEventListener('fullscreenchange', handler);
|
|
2277
|
+
return () => {
|
|
2278
|
+
document.removeEventListener('fullscreenchange', handler);
|
|
2279
|
+
};
|
|
2280
|
+
}, []);
|
|
2281
|
+
return react.useCallback(() => {
|
|
2282
|
+
if (isFullscreen) {
|
|
2283
|
+
document.exitFullscreen().catch((err) => {
|
|
2284
|
+
console.error('Failed to exit fullscreen', err);
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
else {
|
|
2288
|
+
participantViewElement?.requestFullscreen().catch((err) => {
|
|
2289
|
+
console.error('Failed to enter fullscreen', err);
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
}, [isFullscreen, participantViewElement]);
|
|
2293
|
+
};
|
|
2294
|
+
const formatDuration = (durationInMs) => {
|
|
2295
|
+
const days = Math.floor(durationInMs / 86400);
|
|
2296
|
+
const hours = Math.floor(durationInMs / 3600);
|
|
2297
|
+
const minutes = Math.floor((durationInMs % 3600) / 60);
|
|
2298
|
+
const seconds = durationInMs % 60;
|
|
2299
|
+
return `${days ? days + ' ' : ''}${hours ? hours + ':' : ''}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
const GROUP_SIZE = 16;
|
|
2303
|
+
const PaginatedGridLayoutGroup = ({ group, mirror, VideoPlaceholder, PictureInPicturePlaceholder, ParticipantViewUI, }) => {
|
|
2304
|
+
return (jsxRuntime.jsx("div", { className: clsx('str-video__paginated-grid-layout__group', {
|
|
2305
|
+
'str-video__paginated-grid-layout--one': group.length === 1,
|
|
2306
|
+
'str-video__paginated-grid-layout--two-four': group.length >= 2 && group.length <= 4,
|
|
2307
|
+
'str-video__paginated-grid-layout--five-nine': group.length >= 5 && group.length <= 9,
|
|
2308
|
+
}), children: group.map((participant) => (jsxRuntime.jsx(ParticipantView, { participant: participant, muteAudio: true, mirror: mirror, VideoPlaceholder: VideoPlaceholder, PictureInPicturePlaceholder: PictureInPicturePlaceholder, ParticipantViewUI: ParticipantViewUI }, participant.sessionId))) }));
|
|
2309
|
+
};
|
|
2310
|
+
const PaginatedGridLayout = (props) => {
|
|
2311
|
+
const { groupSize = (props.groupSize || 0) > 0
|
|
2312
|
+
? props.groupSize || GROUP_SIZE
|
|
2313
|
+
: GROUP_SIZE, excludeLocalParticipant = false, filterParticipants, mirrorLocalParticipantVideo = true, pageArrowsVisible = true, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, PictureInPicturePlaceholder, muted, } = props;
|
|
2314
|
+
const [page, setPage] = react.useState(0);
|
|
2315
|
+
const [paginatedGridLayoutWrapperElement, setPaginatedGridLayoutWrapperElement,] = react.useState(null);
|
|
2316
|
+
const call = videoReactBindings.useCall();
|
|
2317
|
+
const remoteParticipants = useRawRemoteParticipants();
|
|
2318
|
+
const participants = useFilteredParticipants({
|
|
2319
|
+
excludeLocalParticipant,
|
|
2320
|
+
filterParticipants,
|
|
2321
|
+
});
|
|
2322
|
+
usePaginatedLayoutSortPreset(call);
|
|
2323
|
+
react.useEffect(() => {
|
|
2324
|
+
if (!paginatedGridLayoutWrapperElement || !call)
|
|
2325
|
+
return;
|
|
2326
|
+
const cleanup = call.setViewport(paginatedGridLayoutWrapperElement);
|
|
2327
|
+
return () => cleanup();
|
|
2328
|
+
}, [paginatedGridLayoutWrapperElement, call]);
|
|
2329
|
+
// only used to render video elements
|
|
2330
|
+
const participantGroups = react.useMemo(() => chunk(participants, groupSize), [participants, groupSize]);
|
|
2331
|
+
const pageCount = participantGroups.length;
|
|
2332
|
+
// update page when page count is reduced and selected page no longer exists
|
|
2333
|
+
react.useEffect(() => {
|
|
2334
|
+
if (page > pageCount - 1) {
|
|
2335
|
+
setPage(Math.max(0, pageCount - 1));
|
|
2336
|
+
}
|
|
2337
|
+
}, [page, pageCount]);
|
|
2338
|
+
const selectedGroup = participantGroups[page];
|
|
2339
|
+
const mirror = mirrorLocalParticipantVideo ? undefined : false;
|
|
2340
|
+
if (!call)
|
|
2341
|
+
return null;
|
|
2342
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout__wrapper", ref: setPaginatedGridLayoutWrapperElement, children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout", children: [pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { icon: "caret-left", disabled: page === 0, onClick: () => setPage((currentPage) => Math.max(0, currentPage - 1)) })), selectedGroup && (jsxRuntime.jsx(PaginatedGridLayoutGroup, { group: selectedGroup, mirror: mirror, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI, PictureInPicturePlaceholder: PictureInPicturePlaceholder })), pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { disabled: page === pageCount - 1, icon: "caret-right", onClick: () => setPage((currentPage) => Math.min(pageCount - 1, currentPage + 1)) }))] })] }));
|
|
2343
|
+
};
|
|
2344
|
+
PaginatedGridLayout.displayName = 'PaginatedGridLayout';
|
|
2345
|
+
|
|
2346
|
+
const useCalculateHardLimit = (
|
|
2347
|
+
/**
|
|
2348
|
+
* Element that stretches to 100% of the whole layout component
|
|
2349
|
+
*/
|
|
2350
|
+
wrapperElement,
|
|
2351
|
+
/**
|
|
2352
|
+
* Element that directly hosts individual `ParticipantView` (or wrapper) elements
|
|
2353
|
+
*/
|
|
2354
|
+
hostElement, limit) => {
|
|
2355
|
+
const [calculatedLimit, setCalculatedLimit] = react.useState({
|
|
2356
|
+
vertical: typeof limit === 'number' ? limit : null,
|
|
2357
|
+
horizontal: typeof limit === 'number' ? limit : null,
|
|
2358
|
+
});
|
|
2359
|
+
react.useEffect(() => {
|
|
2360
|
+
if (!hostElement ||
|
|
2361
|
+
!wrapperElement ||
|
|
2362
|
+
typeof limit === 'number' ||
|
|
2363
|
+
typeof limit === 'undefined')
|
|
2364
|
+
return;
|
|
2365
|
+
let childWidth = null;
|
|
2366
|
+
let childHeight = null;
|
|
2367
|
+
const resizeObserver = new ResizeObserver((entries, observer) => {
|
|
2368
|
+
// this part should ideally run as little times as possible
|
|
2369
|
+
// get child measurements and disconnect
|
|
2370
|
+
// does not consider dynamically sized children
|
|
2371
|
+
// this hook is for SpeakerLayout use only, where children in the bar are fixed size
|
|
2372
|
+
if (entries.length > 1) {
|
|
2373
|
+
const child = hostElement.firstChild;
|
|
2374
|
+
if (child) {
|
|
2375
|
+
childHeight = child.clientHeight;
|
|
2376
|
+
childWidth = child.clientWidth;
|
|
2377
|
+
observer.unobserve(hostElement);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
// keep the state at { vertical: 1, horizontal: 1 }
|
|
2381
|
+
// until we get the proper child measurements
|
|
2382
|
+
if (childHeight === null || childWidth === null)
|
|
2383
|
+
return;
|
|
2384
|
+
const vertical = Math.floor(wrapperElement.clientHeight / childHeight);
|
|
2385
|
+
const horizontal = Math.floor(wrapperElement.clientWidth / childWidth);
|
|
2386
|
+
setCalculatedLimit((pv) => {
|
|
2387
|
+
if (pv.vertical !== vertical || pv.horizontal !== horizontal)
|
|
2388
|
+
return { vertical, horizontal };
|
|
2389
|
+
return pv;
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
resizeObserver.observe(wrapperElement);
|
|
2393
|
+
resizeObserver.observe(hostElement);
|
|
2394
|
+
return () => {
|
|
2395
|
+
resizeObserver.disconnect();
|
|
2396
|
+
};
|
|
2397
|
+
}, [hostElement, limit, wrapperElement]);
|
|
2398
|
+
return calculatedLimit;
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const DefaultParticipantViewUIBar = () => (jsxRuntime.jsx(DefaultParticipantViewUI, { menuPlacement: "top-end" }));
|
|
2402
|
+
const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, ParticipantViewUISpotlight = DefaultParticipantViewUI, VideoPlaceholder, PictureInPicturePlaceholder, participantsBarPosition = 'bottom', participantsBarLimit, mirrorLocalParticipantVideo = true, excludeLocalParticipant = false, filterParticipants, pageArrowsVisible = true, muted, enableDragToScroll = false, }) => {
|
|
2403
|
+
const call = videoReactBindings.useCall();
|
|
2404
|
+
const { useParticipants } = videoReactBindings.useCallStateHooks();
|
|
2405
|
+
const allParticipants = useParticipants();
|
|
2406
|
+
const remoteParticipants = useRawRemoteParticipants();
|
|
2407
|
+
const [participantInSpotlight, ...otherParticipants] = useFilteredParticipants({ excludeLocalParticipant, filterParticipants });
|
|
2408
|
+
const [participantsBarWrapperElement, setParticipantsBarWrapperElement] = react.useState(null);
|
|
2409
|
+
const [participantsBarElement, setParticipantsBarElement] = react.useState(null);
|
|
2410
|
+
const [buttonsWrapperElement, setButtonsWrapperElement] = react.useState(null);
|
|
2411
|
+
const isSpeakerScreenSharing = participantInSpotlight && videoClient.hasScreenShare(participantInSpotlight);
|
|
2412
|
+
const hardLimit = useCalculateHardLimit(buttonsWrapperElement, participantsBarElement, participantsBarLimit);
|
|
2413
|
+
const isVertical = participantsBarPosition === 'left' || participantsBarPosition === 'right';
|
|
2414
|
+
const isHorizontal = participantsBarPosition === 'top' || participantsBarPosition === 'bottom';
|
|
2415
|
+
react.useEffect(() => {
|
|
2416
|
+
if (!participantsBarWrapperElement || !call)
|
|
2417
|
+
return;
|
|
2418
|
+
const cleanup = call.setViewport(participantsBarWrapperElement);
|
|
2419
|
+
return () => cleanup();
|
|
2420
|
+
}, [participantsBarWrapperElement, call]);
|
|
2421
|
+
const isOneOnOneCall = allParticipants.length === 2;
|
|
2422
|
+
useSpeakerLayoutSortPreset(call, isOneOnOneCall);
|
|
2423
|
+
useDragToScroll(participantsBarWrapperElement, {
|
|
2424
|
+
enabled: enableDragToScroll,
|
|
2425
|
+
});
|
|
2426
|
+
let participantsWithAppliedLimit = otherParticipants;
|
|
2427
|
+
const hardLimitToApply = isVertical
|
|
2428
|
+
? hardLimit.vertical
|
|
2429
|
+
: hardLimit.horizontal;
|
|
2430
|
+
if (typeof participantsBarLimit !== 'undefined' &&
|
|
2431
|
+
hardLimitToApply !== null) {
|
|
2432
|
+
participantsWithAppliedLimit = otherParticipants.slice(0,
|
|
2433
|
+
// subtract 1 if speaker is sharing screen as
|
|
2434
|
+
// that one is rendered independently from otherParticipants array
|
|
2435
|
+
hardLimitToApply - (isSpeakerScreenSharing ? 1 : 0));
|
|
2436
|
+
}
|
|
2437
|
+
const mirror = mirrorLocalParticipantVideo ? undefined : false;
|
|
2438
|
+
if (!call)
|
|
2439
|
+
return null;
|
|
2440
|
+
const renderParticipantsBar = participantsBarPosition &&
|
|
2441
|
+
(participantsWithAppliedLimit.length > 0 || isSpeakerScreenSharing);
|
|
2442
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__speaker-layout__wrapper", children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: clsx('str-video__speaker-layout', participantsBarPosition &&
|
|
2443
|
+
`str-video__speaker-layout--variant-${participantsBarPosition}`), children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__spotlight", children: participantInSpotlight && (jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, muteAudio: true, mirror: mirror, trackType: isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack', ParticipantViewUI: ParticipantViewUISpotlight, VideoPlaceholder: VideoPlaceholder, PictureInPicturePlaceholder: PictureInPicturePlaceholder })) }), renderParticipantsBar && (jsxRuntime.jsxs("div", { ref: setButtonsWrapperElement, className: "str-video__speaker-layout__participants-bar-buttons-wrapper", children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participants-bar-wrapper", ref: setParticipantsBarWrapperElement, children: jsxRuntime.jsxs("div", { ref: setParticipantsBarElement, className: "str-video__speaker-layout__participants-bar", children: [isSpeakerScreenSharing && (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, PictureInPicturePlaceholder: PictureInPicturePlaceholder, mirror: mirror, muteAudio: true }) }, participantInSpotlight.sessionId)), participantsWithAppliedLimit.map((participant) => (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participant, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, PictureInPicturePlaceholder: PictureInPicturePlaceholder, mirror: mirror, muteAudio: true }) }, participant.sessionId)))] }) }), pageArrowsVisible && isVertical && (jsxRuntime.jsx(VerticalScrollButtons, { scrollWrapper: participantsBarWrapperElement })), pageArrowsVisible && isHorizontal && (jsxRuntime.jsx(HorizontalScrollButtons, { scrollWrapper: participantsBarWrapperElement }))] }))] })] }));
|
|
2444
|
+
};
|
|
2445
|
+
SpeakerLayout.displayName = 'SpeakerLayout';
|
|
2446
|
+
const HorizontalScrollButtons = ({ scrollWrapper, }) => {
|
|
2447
|
+
const scrollPosition = useHorizontalScrollPosition(scrollWrapper);
|
|
2448
|
+
const scrollStartClickHandler = () => {
|
|
2449
|
+
scrollWrapper?.scrollBy({ left: -150, behavior: 'smooth' });
|
|
2450
|
+
};
|
|
2451
|
+
const scrollEndClickHandler = () => {
|
|
2452
|
+
scrollWrapper?.scrollBy({ left: 150, behavior: 'smooth' });
|
|
2453
|
+
};
|
|
2454
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [scrollPosition && scrollPosition !== 'start' && (jsxRuntime.jsx(IconButton, { onClick: scrollStartClickHandler, icon: "caret-left", className: "str-video__speaker-layout__participants-bar--button-left" })), scrollPosition && scrollPosition !== 'end' && (jsxRuntime.jsx(IconButton, { onClick: scrollEndClickHandler, icon: "caret-right", className: "str-video__speaker-layout__participants-bar--button-right" }))] }));
|
|
2455
|
+
};
|
|
2456
|
+
const VerticalScrollButtons = ({ scrollWrapper, }) => {
|
|
2457
|
+
const scrollPosition = useVerticalScrollPosition(scrollWrapper);
|
|
2458
|
+
const scrollTopClickHandler = () => {
|
|
2459
|
+
scrollWrapper?.scrollBy({ top: -150, behavior: 'smooth' });
|
|
2460
|
+
};
|
|
2461
|
+
const scrollBottomClickHandler = () => {
|
|
2462
|
+
scrollWrapper?.scrollBy({ top: 150, behavior: 'smooth' });
|
|
2463
|
+
};
|
|
2464
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [scrollPosition && scrollPosition !== 'top' && (jsxRuntime.jsx(IconButton, { onClick: scrollTopClickHandler, icon: "caret-up", className: "str-video__speaker-layout__participants-bar--button-top" })), scrollPosition && scrollPosition !== 'bottom' && (jsxRuntime.jsx(IconButton, { onClick: scrollBottomClickHandler, icon: "caret-down", className: "str-video__speaker-layout__participants-bar--button-bottom" }))] }));
|
|
2465
|
+
};
|
|
2466
|
+
|
|
2467
|
+
const defaultConfiguration = {
|
|
2468
|
+
layout: 'SpeakerTop',
|
|
2469
|
+
onError: undefined,
|
|
2470
|
+
};
|
|
2471
|
+
const ConfigurationContext = react.createContext(defaultConfiguration);
|
|
2472
|
+
const ConfigurationProvider = ({ children, layout = 'SpeakerTop', onError, }) => {
|
|
2473
|
+
const value = react.useMemo(() => ({ layout, onError }), [layout, onError]);
|
|
2474
|
+
return (jsxRuntime.jsx(ConfigurationContext.Provider, { value: value, children: children }));
|
|
2475
|
+
};
|
|
2476
|
+
/**
|
|
2477
|
+
* Hook to access embedded configuration settings.
|
|
2478
|
+
*/
|
|
2479
|
+
const useEmbeddedConfiguration = () => {
|
|
2480
|
+
return react.useContext(ConfigurationContext);
|
|
2481
|
+
};
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* Hook that creates a StreamVideoClient and connects the user.
|
|
2485
|
+
* Disconnects and cleans up on unmount or when props change.
|
|
2486
|
+
*
|
|
2487
|
+
*/
|
|
2488
|
+
const useInitializeVideoClient = ({ apiKey, user, token, tokenProvider, logLevel, handleError, }) => {
|
|
2489
|
+
const [client, setClient] = react.useState();
|
|
2490
|
+
const tokenProviderRef = react.useRef(tokenProvider);
|
|
2491
|
+
tokenProviderRef.current = tokenProvider;
|
|
2492
|
+
react.useEffect(() => {
|
|
2493
|
+
if (!apiKey)
|
|
2494
|
+
return;
|
|
2495
|
+
const options = logLevel ? { logLevel } : undefined;
|
|
2496
|
+
let _client;
|
|
2497
|
+
try {
|
|
2498
|
+
if (user?.type === 'guest') {
|
|
2499
|
+
const streamUser = {
|
|
2500
|
+
id: user.id,
|
|
2501
|
+
type: 'guest',
|
|
2502
|
+
name: user.name,
|
|
2503
|
+
image: user.image,
|
|
2504
|
+
};
|
|
2505
|
+
_client = new videoClient.StreamVideoClient({
|
|
2506
|
+
apiKey,
|
|
2507
|
+
user: streamUser,
|
|
2508
|
+
options,
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
else if (user?.type === 'anonymous') {
|
|
2512
|
+
const streamUser = {
|
|
2513
|
+
id: '!anon',
|
|
2514
|
+
type: 'anonymous',
|
|
2515
|
+
name: user.name,
|
|
2516
|
+
image: user.image,
|
|
2517
|
+
};
|
|
2518
|
+
_client = new videoClient.StreamVideoClient({
|
|
2519
|
+
apiKey,
|
|
2520
|
+
user: streamUser,
|
|
2521
|
+
token,
|
|
2522
|
+
tokenProvider: tokenProviderRef.current,
|
|
2523
|
+
options,
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
else {
|
|
2527
|
+
const streamUser = {
|
|
2528
|
+
id: user.id,
|
|
2529
|
+
name: user.name,
|
|
2530
|
+
image: user.image,
|
|
2531
|
+
};
|
|
2532
|
+
const currentTokenProvider = tokenProviderRef.current;
|
|
2533
|
+
_client = new videoClient.StreamVideoClient({
|
|
2534
|
+
apiKey,
|
|
2535
|
+
user: streamUser,
|
|
2536
|
+
...(token
|
|
2537
|
+
? {
|
|
2538
|
+
token,
|
|
2539
|
+
...(currentTokenProvider
|
|
2540
|
+
? { tokenProvider: currentTokenProvider }
|
|
2541
|
+
: {}),
|
|
2542
|
+
}
|
|
2543
|
+
: { tokenProvider: currentTokenProvider }),
|
|
2544
|
+
options,
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
setClient(_client);
|
|
2548
|
+
}
|
|
2549
|
+
catch (err) {
|
|
2550
|
+
handleError(err);
|
|
2551
|
+
}
|
|
2552
|
+
return () => {
|
|
2553
|
+
_client
|
|
2554
|
+
?.disconnectUser()
|
|
2555
|
+
.catch((err) => console.error('Failed to disconnect user:', err));
|
|
2556
|
+
setClient(undefined);
|
|
2557
|
+
};
|
|
2558
|
+
}, [
|
|
2559
|
+
apiKey,
|
|
2560
|
+
user.id,
|
|
2561
|
+
user.type,
|
|
2562
|
+
user.name,
|
|
2563
|
+
user.image,
|
|
2564
|
+
token,
|
|
2565
|
+
logLevel,
|
|
2566
|
+
handleError,
|
|
2567
|
+
]);
|
|
2568
|
+
return client;
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
/**
|
|
2572
|
+
* Hook to initialize and manage a Call instance.
|
|
2573
|
+
*/
|
|
2574
|
+
const useInitializeCall = ({ client, callType, callId, handleError, }) => {
|
|
2575
|
+
const [call, setCall] = react.useState();
|
|
2576
|
+
react.useEffect(() => {
|
|
2577
|
+
if (!client || !callId)
|
|
2578
|
+
return;
|
|
2579
|
+
let cancelled = false;
|
|
2580
|
+
const _call = client.call(callType, callId);
|
|
2581
|
+
_call
|
|
2582
|
+
.get()
|
|
2583
|
+
.then(() => {
|
|
2584
|
+
if (!cancelled)
|
|
2585
|
+
setCall(_call);
|
|
2586
|
+
})
|
|
2587
|
+
.catch((err) => {
|
|
2588
|
+
if (cancelled)
|
|
2589
|
+
return;
|
|
2590
|
+
handleError(err);
|
|
2591
|
+
});
|
|
2592
|
+
return () => {
|
|
2593
|
+
cancelled = true;
|
|
2594
|
+
setCall(undefined);
|
|
2595
|
+
if (_call.state.callingState !== videoClient.CallingState.LEFT) {
|
|
2596
|
+
_call
|
|
2597
|
+
.leave()
|
|
2598
|
+
.catch((err) => console.error('Failed to leave call:', err));
|
|
2599
|
+
}
|
|
2600
|
+
};
|
|
2601
|
+
}, [client, callType, callId, handleError]);
|
|
2602
|
+
return call;
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
const BlurToggleButton = () => {
|
|
2606
|
+
const { t } = videoReactBindings.useI18n();
|
|
2607
|
+
const { useCameraState } = videoReactBindings.useCallStateHooks();
|
|
2608
|
+
const { isMute } = useCameraState();
|
|
2609
|
+
const { isSupported, isReady, isLoading, backgroundFilter, applyBackgroundBlurFilter, disableBackgroundFilter, } = useBackgroundFilters();
|
|
2610
|
+
const isBlurred = backgroundFilter === 'blur';
|
|
2611
|
+
const isDisabled = !isReady || isLoading || isMute;
|
|
2612
|
+
const handleClick = react.useCallback(() => {
|
|
2613
|
+
if (isDisabled)
|
|
2614
|
+
return;
|
|
2615
|
+
if (isBlurred) {
|
|
2616
|
+
disableBackgroundFilter();
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
applyBackgroundBlurFilter('high');
|
|
2620
|
+
}
|
|
2621
|
+
}, [
|
|
2622
|
+
applyBackgroundBlurFilter,
|
|
2623
|
+
disableBackgroundFilter,
|
|
2624
|
+
isBlurred,
|
|
2625
|
+
isDisabled,
|
|
2626
|
+
]);
|
|
2627
|
+
const getLabel = () => {
|
|
2628
|
+
if (isLoading)
|
|
2629
|
+
return t('Applying...');
|
|
2630
|
+
return isBlurred ? t('Disable blur') : t('Blur background');
|
|
2631
|
+
};
|
|
2632
|
+
if (!isSupported)
|
|
2633
|
+
return null;
|
|
2634
|
+
return (jsxRuntime.jsxs("button", { type: "button", className: clsx('str-video__embedded-blur-toggle', isBlurred && 'str-video__embedded-blur-toggle--active'), disabled: isDisabled, "aria-pressed": isBlurred, onClick: handleClick, children: [jsxRuntime.jsx(Icon, { icon: "blur-icon" }), jsxRuntime.jsx("span", { children: getLabel() })] }));
|
|
2635
|
+
};
|
|
2636
|
+
const CameraMenuWithBlur = () => {
|
|
2637
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DeviceSelectorVideo, { visualType: "list" }), jsxRuntime.jsx("div", { className: "str-video__embedded-blur-toggle-container", children: jsxRuntime.jsx(BlurToggleButton, {}) })] }));
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
const CallEndedScreen = ({ onJoin, onFeedback, }) => {
|
|
2641
|
+
const { t } = videoReactBindings.useI18n();
|
|
2642
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__container", children: [jsxRuntime.jsx("h2", { className: "str-video__embedded-call-feedback__title", children: t('Call ended') }), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__ended-actions", children: [onJoin && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__ended-column", children: [jsxRuntime.jsx("p", { className: "str-video__embedded-call-feedback__ended-label", children: t('Left by mistake?') }), jsxRuntime.jsxs("button", { type: "button", className: "str-video__embedded-call-feedback__ended-button", onClick: onJoin, children: [jsxRuntime.jsx(Icon, { icon: "login" }), t('Rejoin call')] })] }), jsxRuntime.jsx("div", { className: "str-video__embedded-call-feedback__ended-divider" })] })), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__ended-column", children: [jsxRuntime.jsx("p", { className: "str-video__embedded-call-feedback__ended-label", children: t('Help us improve') }), jsxRuntime.jsxs("button", { type: "button", className: "str-video__embedded-call-feedback__ended-button", onClick: onFeedback, children: [jsxRuntime.jsx(Icon, { icon: "feedback" }), t('Leave feedback')] })] })] })] }));
|
|
2643
|
+
};
|
|
2644
|
+
|
|
2645
|
+
const StarRating = ({ value, onChange }) => {
|
|
2646
|
+
const { t } = videoReactBindings.useI18n();
|
|
2647
|
+
const [hovered, setHovered] = react.useState(0);
|
|
2648
|
+
const displayValue = hovered || value;
|
|
2649
|
+
const getStarClasses = (star) => clsx('str-video__embedded-call-feedback__star', star <= displayValue && 'str-video__embedded-call-feedback__star--active');
|
|
2650
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__rating-section", children: [jsxRuntime.jsx("p", { className: "str-video__embedded-call-feedback__rating-label", children: t('How was your call quality?') }), jsxRuntime.jsx("div", { className: "str-video__embedded-call-feedback__stars", onMouseLeave: () => setHovered(0), children: [1, 2, 3, 4, 5].map((star) => (jsxRuntime.jsx("button", { type: "button", className: getStarClasses(star), onClick: () => onChange(star), onMouseEnter: () => setHovered(star), "aria-label": t('Rate {{ count }} star', { count: star }), children: jsxRuntime.jsx(Icon, { icon: "star-filled" }) }, star))) })] }));
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
const RatingScreen = ({ onSubmit }) => {
|
|
2654
|
+
const { t } = videoReactBindings.useI18n();
|
|
2655
|
+
const [rating, setRating] = react.useState(0);
|
|
2656
|
+
const [message, setMessage] = react.useState('');
|
|
2657
|
+
const handleSubmit = () => {
|
|
2658
|
+
if (rating > 0)
|
|
2659
|
+
onSubmit(rating, message);
|
|
2660
|
+
};
|
|
2661
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__container", children: [jsxRuntime.jsx("h2", { className: "str-video__embedded-call-feedback__title", children: t('Share your feedback') }), jsxRuntime.jsx(StarRating, { value: rating, onChange: setRating }), jsxRuntime.jsx("textarea", { "aria-label": t('Feedback message'), className: "str-video__embedded-call-feedback__textarea", placeholder: t('Tell us about your experience...'), value: message, onChange: (e) => setMessage(e.target.value), rows: 3 }), jsxRuntime.jsx("div", { className: "str-video__embedded-call-feedback__actions", children: jsxRuntime.jsx("button", { type: "button", className: "str-video__button", onClick: handleSubmit, disabled: rating === 0, children: t('Submit feedback') }) })] }));
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
const ThankYouScreen = () => {
|
|
2665
|
+
const { t } = videoReactBindings.useI18n();
|
|
2666
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-feedback__container", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-call-feedback__checkmark", children: jsxRuntime.jsx(Icon, { icon: "checkmark" }) }), jsxRuntime.jsx("h2", { className: "str-video__embedded-call-feedback__title", children: t('Thank you!') }), jsxRuntime.jsx("p", { className: "str-video__embedded-call-feedback__subtitle", children: t('Your feedback helps improve call quality.') })] }));
|
|
2667
|
+
};
|
|
2668
|
+
|
|
2669
|
+
const CallFeedback = ({ onJoin }) => {
|
|
2670
|
+
const call = videoReactBindings.useCall();
|
|
2671
|
+
const { onError } = useEmbeddedConfiguration();
|
|
2672
|
+
const [state, setState] = react.useState('ended');
|
|
2673
|
+
const onFeedback = react.useCallback(() => setState('rating'), []);
|
|
2674
|
+
const handleSubmit = react.useCallback(async (rating, message) => {
|
|
2675
|
+
if (!call)
|
|
2676
|
+
return;
|
|
2677
|
+
const clampedRating = Math.min(Math.max(1, rating), 5);
|
|
2678
|
+
try {
|
|
2679
|
+
await call.submitFeedback(clampedRating, {
|
|
2680
|
+
reason: message,
|
|
2681
|
+
custom: { message },
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
catch (err) {
|
|
2685
|
+
onError?.(err);
|
|
2686
|
+
}
|
|
2687
|
+
finally {
|
|
2688
|
+
setState('submitted');
|
|
2689
|
+
}
|
|
2690
|
+
}, [call, onError]);
|
|
2691
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-call-feedback", children: state === 'submitted' ? (jsxRuntime.jsx(ThankYouScreen, {})) : state === 'rating' ? (jsxRuntime.jsx(RatingScreen, { onSubmit: handleSubmit })) : (jsxRuntime.jsx(CallEndedScreen, { onJoin: onJoin, onFeedback: onFeedback })) }));
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
const ConnectionNotification = () => {
|
|
2695
|
+
const { t } = videoReactBindings.useI18n();
|
|
2696
|
+
const { useCallCallingState } = videoReactBindings.useCallStateHooks();
|
|
2697
|
+
const callingState = useCallCallingState();
|
|
2698
|
+
const isOffline = callingState === videoClient.CallingState.OFFLINE;
|
|
2699
|
+
const isReconnecting = callingState === videoClient.CallingState.RECONNECTING;
|
|
2700
|
+
const isMigrating = callingState === videoClient.CallingState.MIGRATING;
|
|
2701
|
+
const isJoining = callingState === videoClient.CallingState.JOINING;
|
|
2702
|
+
const hasFailedToRecover = callingState === videoClient.CallingState.RECONNECTING_FAILED;
|
|
2703
|
+
const showError = isOffline || hasFailedToRecover;
|
|
2704
|
+
const showLoading = isJoining || isReconnecting || isMigrating;
|
|
2705
|
+
if (showError) {
|
|
2706
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-connection-notification", children: jsxRuntime.jsx(Notification, { isVisible: true, placement: "bottom", message: isOffline
|
|
2707
|
+
? t('You are offline. Check your internet connection.')
|
|
2708
|
+
: t('Failed to restore connection. Please try again.') }) }));
|
|
2709
|
+
}
|
|
2710
|
+
if (showLoading) {
|
|
2711
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-connection-notification", children: jsxRuntime.jsx(Notification, { isVisible: true, placement: "bottom", iconClassName: null, message: jsxRuntime.jsx(LoadingIndicator, { text: isMigrating
|
|
2712
|
+
? t('Migrating...')
|
|
2713
|
+
: isJoining
|
|
2714
|
+
? t('Joining')
|
|
2715
|
+
: t('Reconnecting...') }) }) }));
|
|
2716
|
+
}
|
|
2717
|
+
return null;
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
const EmbeddedParticipantViewUI = () => {
|
|
2721
|
+
const { participantViewElement, trackType } = useParticipantViewContext();
|
|
2722
|
+
const handleDoubleClick = react.useCallback(() => {
|
|
2723
|
+
if (!participantViewElement)
|
|
2724
|
+
return;
|
|
2725
|
+
if (typeof participantViewElement.requestFullscreen === 'undefined')
|
|
2726
|
+
return;
|
|
2727
|
+
if (!document.fullscreenElement) {
|
|
2728
|
+
participantViewElement.requestFullscreen().catch(console.error);
|
|
2729
|
+
}
|
|
2730
|
+
else {
|
|
2731
|
+
document.exitFullscreen().catch(console.error);
|
|
2732
|
+
}
|
|
2733
|
+
}, [participantViewElement]);
|
|
2734
|
+
return (jsxRuntime.jsx("div", { className: clsx('str-video__embedded-participant-view-ui', trackType === 'screenShareTrack' &&
|
|
2735
|
+
'str-video__embedded-participant-view-ui--screen-share'), onDoubleClick: handleDoubleClick, children: jsxRuntime.jsx(DefaultParticipantViewUI, {}) }));
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
const JoinError = ({ onJoin }) => {
|
|
2739
|
+
const { t } = videoReactBindings.useI18n();
|
|
2740
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-join-error", children: [jsxRuntime.jsx("h2", { className: "str-video__embedded-join-error__title", children: t('Failed to join the call') }), jsxRuntime.jsx("p", { className: "str-video__embedded-join-error__message", children: t("We couldn't connect to the server. Please check your connection and try again.") }), jsxRuntime.jsxs("button", { type: "button", className: "str-video__button", onClick: onJoin, children: [jsxRuntime.jsx(Icon, { icon: "login" }), t('Try again')] })] }));
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
const ToggleMenuButton$1 = react.forwardRef(function ToggleMenuButton(props, ref) {
|
|
2744
|
+
const { useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
2745
|
+
const { selectedDevice: selectedMic, devices: microphones } = useMicrophoneState();
|
|
2746
|
+
return (jsxRuntime.jsxs("button", { ref: ref, className: "str-video__embedded-lobby__device-button", children: [jsxRuntime.jsx(Icon, { className: "str-video__embedded-lobby__device-button-icon", icon: "mic" }), jsxRuntime.jsx("span", { className: "str-video__embedded-lobby__device-button-label", children: microphones?.find((m) => m.deviceId === selectedMic)
|
|
2747
|
+
?.label || 'Default' }), jsxRuntime.jsx(Icon, { icon: props.menuShown ? 'chevron-down' : 'chevron-up' })] }));
|
|
2748
|
+
});
|
|
2749
|
+
const ToggleMicButton = () => {
|
|
2750
|
+
const { t } = videoReactBindings.useI18n();
|
|
2751
|
+
return (jsxRuntime.jsxs(MenuToggle, { placement: "top-start", ToggleButton: ToggleMenuButton$1, visualType: MenuVisualType.MENU, children: [jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list", title: t('Microphone') }), jsxRuntime.jsx(DeviceSelectorAudioOutput, { visualType: "list", title: t('Speaker') })] }));
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
const ToggleMenuButton = react.forwardRef(function ToggleMenuButton(props, ref) {
|
|
2755
|
+
const { useCameraState } = videoReactBindings.useCallStateHooks();
|
|
2756
|
+
const { selectedDevice: selectedCamera, devices: cameras } = useCameraState();
|
|
2757
|
+
return (jsxRuntime.jsxs("button", { ref: ref, className: "str-video__embedded-lobby__device-button", children: [jsxRuntime.jsx(Icon, { className: "str-video__embedded-lobby__device-button-icon", icon: "camera" }), jsxRuntime.jsx("span", { className: "str-video__embedded-lobby__device-button-label", children: cameras?.find((c) => c.deviceId === selectedCamera)
|
|
2758
|
+
?.label || 'Default' }), jsxRuntime.jsx(Icon, { icon: props.menuShown ? 'chevron-down' : 'chevron-up' })] }));
|
|
2759
|
+
});
|
|
2760
|
+
const ToggleCameraButton = () => {
|
|
2761
|
+
return (jsxRuntime.jsx(MenuToggle, { placement: "top-start", ToggleButton: ToggleMenuButton, visualType: MenuVisualType.MENU, children: jsxRuntime.jsx(CameraMenuWithBlur, {}) }));
|
|
2762
|
+
};
|
|
2763
|
+
|
|
2764
|
+
const DisabledDeviceButton = ({ icon, label, }) => (jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__device-button str-video__embedded-lobby__device-button--disabled", children: [jsxRuntime.jsx(Icon, { className: "str-video__embedded-lobby__device-button-icon", icon: icon }), jsxRuntime.jsx("span", { className: "str-video__embedded-lobby__device-button-label", children: label })] }));
|
|
2765
|
+
|
|
2766
|
+
const DisabledVideoPreview = () => {
|
|
2767
|
+
const { t } = videoReactBindings.useI18n();
|
|
2768
|
+
const user = videoReactBindings.useConnectedUser();
|
|
2769
|
+
const { useCameraState, useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
2770
|
+
const { hasBrowserPermission: hasCameraPermission } = useCameraState();
|
|
2771
|
+
const { hasBrowserPermission: hasMicPermission } = useMicrophoneState();
|
|
2772
|
+
const hasBrowserMediaPermission = hasCameraPermission && hasMicPermission;
|
|
2773
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-lobby__no-permission", children: hasBrowserMediaPermission ? (jsxRuntime.jsx(Avatar, { imageSrc: user?.image, name: user?.name || user?.id })) : (t('Please grant your browser permission to access your camera and microphone.')) }));
|
|
2774
|
+
};
|
|
2775
|
+
const NoCameraPreview = () => {
|
|
2776
|
+
const { t } = videoReactBindings.useI18n();
|
|
2777
|
+
const { useCameraState, useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
2778
|
+
const { hasBrowserPermission: hasCameraPermission } = useCameraState();
|
|
2779
|
+
const { hasBrowserPermission: hasMicPermission } = useMicrophoneState();
|
|
2780
|
+
const hasBrowserMediaPermission = hasCameraPermission && hasMicPermission;
|
|
2781
|
+
if (!hasBrowserMediaPermission) {
|
|
2782
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-lobby__no-permission", children: t('Please grant your browser permission to access your camera and microphone.') }));
|
|
2783
|
+
}
|
|
2784
|
+
return (jsxRuntime.jsx("div", { className: "str-video__video-preview__no-camera-preview", children: t('No camera found') }));
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
const DeviceControls = ({ isVideoEnabled }) => {
|
|
2788
|
+
const { t } = videoReactBindings.useI18n();
|
|
2789
|
+
const { useCameraState, useMicrophoneState } = videoReactBindings.useCallStateHooks();
|
|
2790
|
+
const { hasBrowserPermission: hasCameraPermission } = useCameraState();
|
|
2791
|
+
const { hasBrowserPermission: hasMicPermission } = useMicrophoneState();
|
|
2792
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__video-preview", children: [jsxRuntime.jsx(VideoPreview, { DisabledVideoPreview: DisabledVideoPreview, NoCameraPreview: NoCameraPreview }), jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__media-toggle", children: [jsxRuntime.jsx(ToggleAudioPreviewButton, { Menu: null }), isVideoEnabled && jsxRuntime.jsx(ToggleVideoPreviewButton, { Menu: null })] })] }), jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__media", children: [hasMicPermission ? (jsxRuntime.jsx(ToggleMicButton, {})) : (jsxRuntime.jsx(DisabledDeviceButton, { icon: "mic", label: t('Permission needed') })), isVideoEnabled &&
|
|
2793
|
+
(hasCameraPermission ? (jsxRuntime.jsx(ToggleCameraButton, {})) : (jsxRuntime.jsx(DisabledDeviceButton, { icon: "camera", label: t('Permission needed') })))] })] }));
|
|
2794
|
+
};
|
|
2795
|
+
|
|
2796
|
+
/**
|
|
2797
|
+
* Lobby component - Device setup screen before joining a call.
|
|
2798
|
+
*/
|
|
2799
|
+
const Lobby = ({ onJoin, title, joinLabel }) => {
|
|
2800
|
+
const { t } = videoReactBindings.useI18n();
|
|
2801
|
+
const user = videoReactBindings.useConnectedUser();
|
|
2802
|
+
const { useCameraState, useCallSettings } = videoReactBindings.useCallStateHooks();
|
|
2803
|
+
const { isMute } = useCameraState();
|
|
2804
|
+
const settings = useCallSettings();
|
|
2805
|
+
const isVideoEnabled = settings?.video.enabled ?? true;
|
|
2806
|
+
const resolvedJoinLabel = joinLabel ?? t('Join');
|
|
2807
|
+
const resolvedTitle = title ?? t('Set up your call before joining');
|
|
2808
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-lobby", children: jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__content", children: [jsxRuntime.jsx("h1", { className: "str-video__embedded-lobby__heading", children: resolvedTitle }), jsxRuntime.jsx("div", { className: clsx('str-video__embedded-lobby__camera', isMute && 'str-video__embedded-lobby__camera--off'), children: jsxRuntime.jsx(DeviceControls, { isVideoEnabled: isVideoEnabled }) }), jsxRuntime.jsxs("div", { className: "str-video__embedded-lobby__display-name", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-lobby__display-name-label", children: t('Display name') }), jsxRuntime.jsx("span", { className: "str-video__embedded-lobby__display-name-value", children: user?.name }), jsxRuntime.jsxs("button", { className: "str-video__button", onClick: onJoin, children: [jsxRuntime.jsx(Icon, { className: "str-video__button__icon", icon: "login" }), resolvedJoinLabel] })] })] }) }));
|
|
2809
|
+
};
|
|
2810
|
+
|
|
2811
|
+
const ViewersCount = ({ count }) => (jsxRuntime.jsxs("div", { className: "str-video__embedded-livestream-duration__viewers", children: [jsxRuntime.jsx(Icon, { icon: "eye", className: "str-video__embedded-livestream-duration__eye-icon" }), jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__count", children: videoClient.humanize(count) })] }));
|
|
2812
|
+
|
|
2813
|
+
const Layouts = {
|
|
2814
|
+
Livestream: {
|
|
2815
|
+
Component: LivestreamLayout,
|
|
2816
|
+
props: {
|
|
2817
|
+
showLiveBadge: false,
|
|
2818
|
+
showParticipantCount: false,
|
|
2819
|
+
showDuration: false,
|
|
2820
|
+
},
|
|
2821
|
+
},
|
|
2822
|
+
PaginatedGrid: {
|
|
2823
|
+
Component: PaginatedGridLayout,
|
|
2824
|
+
props: { groupSize: 16, ParticipantViewUI: EmbeddedParticipantViewUI },
|
|
2825
|
+
},
|
|
2826
|
+
SpeakerLeft: {
|
|
2827
|
+
Component: SpeakerLayout,
|
|
2828
|
+
props: {
|
|
2829
|
+
participantsBarPosition: 'left',
|
|
2830
|
+
ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
|
|
2831
|
+
ParticipantViewUIBar: EmbeddedParticipantViewUI,
|
|
2832
|
+
},
|
|
2833
|
+
},
|
|
2834
|
+
SpeakerRight: {
|
|
2835
|
+
Component: SpeakerLayout,
|
|
2836
|
+
props: {
|
|
2837
|
+
participantsBarPosition: 'right',
|
|
2838
|
+
ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
|
|
2839
|
+
ParticipantViewUIBar: EmbeddedParticipantViewUI,
|
|
2840
|
+
},
|
|
2841
|
+
},
|
|
2842
|
+
SpeakerTop: {
|
|
2843
|
+
Component: SpeakerLayout,
|
|
2844
|
+
props: {
|
|
2845
|
+
participantsBarPosition: 'top',
|
|
2846
|
+
ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
|
|
2847
|
+
ParticipantViewUIBar: EmbeddedParticipantViewUI,
|
|
2848
|
+
},
|
|
2849
|
+
},
|
|
2850
|
+
SpeakerBottom: {
|
|
2851
|
+
Component: SpeakerLayout,
|
|
2852
|
+
props: {
|
|
2853
|
+
participantsBarPosition: 'bottom',
|
|
2854
|
+
ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
|
|
2855
|
+
ParticipantViewUIBar: EmbeddedParticipantViewUI,
|
|
2856
|
+
},
|
|
2857
|
+
},
|
|
2858
|
+
};
|
|
2859
|
+
const EMPTY_PROPS = {};
|
|
2860
|
+
const VALID_LAYOUTS = Object.keys(Layouts);
|
|
2861
|
+
const isValidLayout = (layout) => VALID_LAYOUTS.includes(layout);
|
|
2862
|
+
/**
|
|
2863
|
+
* Hook to manage layout selection.
|
|
2864
|
+
* Returns the layout Component and its props.
|
|
2865
|
+
*/
|
|
2866
|
+
const useLayout = () => {
|
|
2867
|
+
const { layout: configuredLayout } = useEmbeddedConfiguration();
|
|
2868
|
+
const { useHasOngoingScreenShare } = videoReactBindings.useCallStateHooks();
|
|
2869
|
+
const hasScreenShare = useHasOngoingScreenShare();
|
|
2870
|
+
const defaultLayout = isValidLayout(configuredLayout ?? '')
|
|
2871
|
+
? configuredLayout
|
|
2872
|
+
: 'SpeakerTop';
|
|
2873
|
+
const [layout, setLayout] = react.useState(defaultLayout);
|
|
2874
|
+
react.useEffect(() => {
|
|
2875
|
+
if (hasScreenShare) {
|
|
2876
|
+
setLayout((currentLayout) => {
|
|
2877
|
+
if (currentLayout.startsWith('Speaker'))
|
|
2878
|
+
return currentLayout;
|
|
2879
|
+
return 'SpeakerRight';
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
else {
|
|
2883
|
+
setLayout(defaultLayout);
|
|
2884
|
+
}
|
|
2885
|
+
}, [hasScreenShare, defaultLayout]);
|
|
2886
|
+
const { Component, props = EMPTY_PROPS } = Layouts[layout];
|
|
2887
|
+
return { Component, props };
|
|
2888
|
+
};
|
|
2889
|
+
|
|
2890
|
+
/**
|
|
2891
|
+
* Hook that lazily loads the noise cancellation module from @stream-io/audio-filters-web.
|
|
2892
|
+
* Skips loading if the server-side noise cancellation setting is disabled.
|
|
2893
|
+
* Returns the NoiseCancellation instance when loaded, or undefined if unavailable.
|
|
2894
|
+
* The `ready` flag becomes `true` once loading completes (even on failure),
|
|
2895
|
+
* or immediately if noise cancellation is disabled by server settings.
|
|
2896
|
+
*/
|
|
2897
|
+
const useNoiseCancellationLoader = (call) => {
|
|
2898
|
+
const [noiseCancellation, setNoiseCancellation] = react.useState();
|
|
2899
|
+
const [ready, setReady] = react.useState(false);
|
|
2900
|
+
const ncLoader = react.useRef(undefined);
|
|
2901
|
+
const ncSettings = call?.state.settings?.audio?.noise_cancellation;
|
|
2902
|
+
const isNoiseCancellationEnabled = !!(ncSettings && ncSettings.mode !== videoClient.NoiseCancellationSettingsModeEnum.DISABLED);
|
|
2903
|
+
react.useEffect(() => {
|
|
2904
|
+
if (!call)
|
|
2905
|
+
return;
|
|
2906
|
+
if (!isNoiseCancellationEnabled) {
|
|
2907
|
+
setReady(true);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const load = (ncLoader.current || Promise.resolve())
|
|
2911
|
+
.then(() => import('@stream-io/audio-filters-web'))
|
|
2912
|
+
.then(({ NoiseCancellation }) => {
|
|
2913
|
+
const nc = new NoiseCancellation({});
|
|
2914
|
+
setNoiseCancellation(nc);
|
|
2915
|
+
})
|
|
2916
|
+
.catch((err) => {
|
|
2917
|
+
console.warn('[EmbeddedStreamClient] Failed to load noise cancellation. ' +
|
|
2918
|
+
'Make sure @stream-io/audio-filters-web is installed.', err);
|
|
2919
|
+
})
|
|
2920
|
+
.finally(() => {
|
|
2921
|
+
setReady(true);
|
|
2922
|
+
});
|
|
2923
|
+
return () => {
|
|
2924
|
+
ncLoader.current = load.then(() => {
|
|
2925
|
+
setNoiseCancellation(undefined);
|
|
2926
|
+
setReady(false);
|
|
2927
|
+
});
|
|
2928
|
+
};
|
|
2929
|
+
}, [call, isNoiseCancellationEnabled]);
|
|
2930
|
+
return { noiseCancellation, ready };
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
/**
|
|
2934
|
+
* Hook to prevent screen from going to sleep during active calls.
|
|
2935
|
+
* Uses the Screen Wake Lock API when available.
|
|
2936
|
+
*/
|
|
2937
|
+
const useWakeLock = () => {
|
|
2938
|
+
const { useCallCallingState } = videoReactBindings.useCallStateHooks();
|
|
2939
|
+
const callState = useCallCallingState();
|
|
2940
|
+
react.useEffect(() => {
|
|
2941
|
+
if (callState !== videoClient.CallingState.JOINED || !('wakeLock' in navigator))
|
|
2942
|
+
return;
|
|
2943
|
+
let interrupted = false;
|
|
2944
|
+
let wakeLockSentinel = null;
|
|
2945
|
+
navigator.wakeLock
|
|
2946
|
+
.request('screen')
|
|
2947
|
+
.then((wls) => {
|
|
2948
|
+
if (interrupted)
|
|
2949
|
+
return wls.release();
|
|
2950
|
+
wakeLockSentinel = wls;
|
|
2951
|
+
})
|
|
2952
|
+
.catch((error) => console.debug(`Couldn't setup WakeLock due to: ${error}`));
|
|
2953
|
+
return () => {
|
|
2954
|
+
interrupted = true;
|
|
2955
|
+
wakeLockSentinel?.release();
|
|
2956
|
+
};
|
|
2957
|
+
}, [callState]);
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
const formatElapsed = (seconds) => {
|
|
2961
|
+
const h = Math.floor(seconds / 3600);
|
|
2962
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
2963
|
+
const s = seconds % 60;
|
|
2964
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
2965
|
+
return h > 0 ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
2966
|
+
};
|
|
2967
|
+
/**
|
|
2968
|
+
* Returns a live-updating formatted elapsed duration string
|
|
2969
|
+
* computed from the given start date.
|
|
2970
|
+
*/
|
|
2971
|
+
const useCallDuration = (startedAt) => {
|
|
2972
|
+
const startedAtDate = react.useMemo(() => (startedAt ? new Date(startedAt).getTime() : undefined), [startedAt]);
|
|
2973
|
+
const [elapsed, setElapsed] = react.useState('');
|
|
2974
|
+
react.useEffect(() => {
|
|
2975
|
+
if (!startedAtDate)
|
|
2976
|
+
return;
|
|
2977
|
+
const update = () => {
|
|
2978
|
+
const seconds = Math.max(0, Math.floor((Date.now() - startedAtDate) / 1000));
|
|
2979
|
+
setElapsed(formatElapsed(seconds));
|
|
2980
|
+
};
|
|
2981
|
+
update();
|
|
2982
|
+
const interval = setInterval(update, 1000);
|
|
2983
|
+
return () => clearInterval(interval);
|
|
2984
|
+
}, [startedAtDate]);
|
|
2985
|
+
return { elapsed };
|
|
2986
|
+
};
|
|
2987
|
+
|
|
2988
|
+
/**
|
|
2989
|
+
* Hook that initializes the Stream Video client and call.
|
|
2990
|
+
* Combines useInitializeVideoClient, useInitializeCall, and useNoiseCancellationLoader.
|
|
2991
|
+
*/
|
|
2992
|
+
const useEmbeddedClient = ({ apiKey, user, callId, callType, token, tokenProvider, logLevel, handleError, }) => {
|
|
2993
|
+
const client = useInitializeVideoClient({
|
|
2994
|
+
apiKey,
|
|
2995
|
+
user,
|
|
2996
|
+
token,
|
|
2997
|
+
tokenProvider,
|
|
2998
|
+
logLevel,
|
|
2999
|
+
handleError,
|
|
3000
|
+
});
|
|
3001
|
+
const call = useInitializeCall({
|
|
3002
|
+
client,
|
|
3003
|
+
callType,
|
|
3004
|
+
callId,
|
|
3005
|
+
handleError,
|
|
3006
|
+
});
|
|
3007
|
+
react.useEffect(() => {
|
|
3008
|
+
if (!call)
|
|
3009
|
+
return;
|
|
3010
|
+
call.tracer.trace('embedded.initialized', { callType });
|
|
3011
|
+
}, [call, callType]);
|
|
3012
|
+
const { noiseCancellation, ready: noiseCancellationReady } = useNoiseCancellationLoader(call);
|
|
3013
|
+
return {
|
|
3014
|
+
client,
|
|
3015
|
+
call,
|
|
3016
|
+
noiseCancellation,
|
|
3017
|
+
noiseCancellationReady,
|
|
3018
|
+
};
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
/**
|
|
3022
|
+
* Distinguishes a temporary live pause (host went backstage) from a real
|
|
3023
|
+
* call end. Returns `true` when the viewer was kicked because the host
|
|
3024
|
+
* paused the livestream — not because the call was fully terminated.
|
|
3025
|
+
*
|
|
3026
|
+
* Resets to `false` on rejoin or when `call.ended` (real end) arrives.
|
|
3027
|
+
*/
|
|
3028
|
+
const useIsLivestreamPaused = () => {
|
|
3029
|
+
const call = videoReactBindings.useCall();
|
|
3030
|
+
const { useCallCallingState } = videoReactBindings.useCallStateHooks();
|
|
3031
|
+
const callingState = useCallCallingState();
|
|
3032
|
+
const [isPaused, setIsPaused] = react.useState(false);
|
|
3033
|
+
react.useEffect(() => {
|
|
3034
|
+
if (!call)
|
|
3035
|
+
return;
|
|
3036
|
+
const unsubSfu = call.on('callEnded', (e) => {
|
|
3037
|
+
if (e.reason === videoClient.SfuModels.CallEndedReason.LIVE_ENDED) {
|
|
3038
|
+
setIsPaused(true);
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
const unsubEnded = call.on('call.ended', () => {
|
|
3042
|
+
setIsPaused(false);
|
|
3043
|
+
});
|
|
3044
|
+
return () => {
|
|
3045
|
+
unsubSfu();
|
|
3046
|
+
unsubEnded();
|
|
3047
|
+
};
|
|
3048
|
+
}, [call]);
|
|
3049
|
+
react.useEffect(() => {
|
|
3050
|
+
if (callingState === videoClient.CallingState.JOINED) {
|
|
3051
|
+
setIsPaused(false);
|
|
3052
|
+
}
|
|
3053
|
+
}, [callingState]);
|
|
3054
|
+
return isPaused;
|
|
3055
|
+
};
|
|
3056
|
+
|
|
3057
|
+
const NoiseCancellationWrapper = ({ noiseCancellation, children, }) => {
|
|
3058
|
+
if (!noiseCancellation) {
|
|
3059
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
3060
|
+
}
|
|
3061
|
+
return (jsxRuntime.jsx(NoiseCancellationProvider, { noiseCancellation: noiseCancellation, children: children }));
|
|
3062
|
+
};
|
|
3063
|
+
/**
|
|
3064
|
+
* Shared provider wrapper for embedded components.
|
|
3065
|
+
* Handles client/call initialization and wraps children with all necessary providers.
|
|
3066
|
+
*/
|
|
3067
|
+
const EmbeddedClientProvider = ({ apiKey, user, callId, callType, token, tokenProvider, logLevel, onError, layout, theme, children, }) => {
|
|
3068
|
+
const [showError, setShowError] = react.useState(false);
|
|
3069
|
+
const onErrorStable = videoReactBindings.useEffectEvent(onError ?? console.error);
|
|
3070
|
+
const handleError = react.useCallback((error) => {
|
|
3071
|
+
setShowError(true);
|
|
3072
|
+
onErrorStable(error);
|
|
3073
|
+
}, [onErrorStable]);
|
|
3074
|
+
const { client, call, noiseCancellation, noiseCancellationReady } = useEmbeddedClient({
|
|
3075
|
+
apiKey,
|
|
3076
|
+
user,
|
|
3077
|
+
callId,
|
|
3078
|
+
callType,
|
|
3079
|
+
token,
|
|
3080
|
+
tokenProvider,
|
|
3081
|
+
logLevel,
|
|
3082
|
+
handleError,
|
|
3083
|
+
});
|
|
3084
|
+
if (showError) {
|
|
3085
|
+
return (jsxRuntime.jsx(StreamTheme, { className: "str-video__embedded", children: jsxRuntime.jsx("div", { className: "str-video__embedded-error", children: jsxRuntime.jsx("p", { className: "str-video__embedded-error__message", children: "An error occurred while initializing the client. Please try again later." }) }) }));
|
|
3086
|
+
}
|
|
3087
|
+
if (!call || !client || !noiseCancellationReady) {
|
|
3088
|
+
return (jsxRuntime.jsx(StreamTheme, { className: "str-video__embedded", children: jsxRuntime.jsx(LoadingIndicator, { className: "str-video__embedded-loading" }) }));
|
|
3089
|
+
}
|
|
3090
|
+
return (jsxRuntime.jsx(StreamVideo, { client: client, children: jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(ConfigurationProvider, { layout: layout, onError: onErrorStable, children: jsxRuntime.jsx(BackgroundFiltersProvider, { children: jsxRuntime.jsx(NoiseCancellationWrapper, { noiseCancellation: noiseCancellation, children: jsxRuntime.jsx(StreamTheme, { className: "str-video__embedded", style: theme, children: children }) }) }) }) }) }));
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
/**
|
|
3094
|
+
* Renders the active call control bar
|
|
3095
|
+
*/
|
|
3096
|
+
const CallControls = ({ showParticipants, onToggleParticipants, }) => {
|
|
3097
|
+
const { t } = videoReactBindings.useI18n();
|
|
3098
|
+
const { useCallSession } = videoReactBindings.useCallStateHooks();
|
|
3099
|
+
const session = useCallSession();
|
|
3100
|
+
const startedAt = session?.started_at;
|
|
3101
|
+
const { elapsed } = useCallDuration(startedAt);
|
|
3102
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-controls str-video__call-controls", children: [jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--options", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-mobile", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), startedAt && (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-duration str-video__embedded-desktop", children: [jsxRuntime.jsx(Icon, { icon: "verified", className: "str-video__embedded-call-duration__icon" }), jsxRuntime.jsx("span", { className: "str-video__embedded-call-duration__time", children: elapsed })] }))] }), jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--media", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], hasPermissionsOnly: true, children: jsxRuntime.jsx(MicCaptureErrorNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, { Menu: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list", title: t('Microphone') }), jsxRuntime.jsx(DeviceSelectorAudioOutput, { visualType: "list", title: t('Speaker') })] }), menuPlacement: "top" }) }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], hasPermissionsOnly: true, children: jsxRuntime.jsx(ToggleVideoPublishingButton, { Menu: jsxRuntime.jsx(CameraMenuWithBlur, {}), menuPlacement: "top" }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SCREENSHARE], children: jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(ScreenShareButton, {}) }) }), jsxRuntime.jsx(RecordCallConfirmationButton, {}), jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(CancelCallConfirmButton, {}) })] }), jsxRuntime.jsx("div", { className: "str-video__call-controls--group str-video__call-controls--sidebar", children: jsxRuntime.jsx(WithTooltip, { title: t('Participants'), children: jsxRuntime.jsx(CompositeButton, { active: showParticipants, "aria-label": t('Participants'), "aria-pressed": showParticipants, onClick: onToggleParticipants, children: jsxRuntime.jsx(Icon, { icon: "participants" }) }) }) })] }));
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
/**
|
|
3106
|
+
* Renders the call header bar with elapsed time and leave/end call button.
|
|
3107
|
+
*/
|
|
3108
|
+
const CallHeader = () => {
|
|
3109
|
+
const { useCallSession } = videoReactBindings.useCallStateHooks();
|
|
3110
|
+
const session = useCallSession();
|
|
3111
|
+
const startedAt = session?.started_at;
|
|
3112
|
+
const { elapsed } = useCallDuration(startedAt);
|
|
3113
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-header", children: [startedAt && (jsxRuntime.jsxs("div", { className: "str-video__embedded-call-duration", children: [jsxRuntime.jsx(Icon, { icon: "verified", className: "str-video__embedded-call-duration__icon" }), jsxRuntime.jsx("span", { className: "str-video__embedded-call-duration__time", children: elapsed })] })), jsxRuntime.jsx(CancelCallConfirmButton, {})] }));
|
|
3114
|
+
};
|
|
3115
|
+
|
|
3116
|
+
/**
|
|
3117
|
+
* CallLayout renders the active call experience with layout, controls and sidebar.
|
|
3118
|
+
*/
|
|
3119
|
+
const CallLayout = () => {
|
|
3120
|
+
useWakeLock();
|
|
3121
|
+
const [showParticipants, setShowParticipants] = react.useState(false);
|
|
3122
|
+
const { Component: LayoutComponent, props: layoutProps } = useLayout();
|
|
3123
|
+
const handleToggleParticipants = react.useCallback(() => {
|
|
3124
|
+
setShowParticipants((prev) => !prev);
|
|
3125
|
+
}, []);
|
|
3126
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call", children: [jsxRuntime.jsx(ConnectionNotification, {}), jsxRuntime.jsx(PermissionRequests, {}), jsxRuntime.jsx("div", { className: "str-video__embedded-notifications", children: jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], hasPermissionsOnly: true, children: jsxRuntime.jsx(SpeakingWhileMutedNotification, {}) }) }), jsxRuntime.jsx(CallHeader, {}), jsxRuntime.jsxs("div", { className: "str-video__embedded-layout", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-layout__stage", children: jsxRuntime.jsx(LayoutComponent, { ...layoutProps }) }), jsxRuntime.jsx("div", { className: clsx('str-video__embedded-sidebar', showParticipants && 'str-video__embedded-sidebar--open'), children: showParticipants && (jsxRuntime.jsx("div", { className: "str-video__embedded-participants", children: jsxRuntime.jsx(CallParticipantsList, { onClose: handleToggleParticipants }) })) })] }), jsxRuntime.jsx(CallControls, { showParticipants: showParticipants, onToggleParticipants: handleToggleParticipants })] }));
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
/**
|
|
3130
|
+
* CallStateRouter is the state decider component that manages view state transitions.
|
|
3131
|
+
*/
|
|
3132
|
+
const CallStateRouter = () => {
|
|
3133
|
+
const call = videoReactBindings.useCall();
|
|
3134
|
+
const { useCallCallingState, useLocalParticipant } = videoReactBindings.useCallStateHooks();
|
|
3135
|
+
const localParticipant = useLocalParticipant();
|
|
3136
|
+
const callingState = useCallCallingState();
|
|
3137
|
+
const { onError } = useEmbeddedConfiguration();
|
|
3138
|
+
const [joinError, setJoinError] = react.useState(false);
|
|
3139
|
+
const handleJoin = react.useCallback(async () => {
|
|
3140
|
+
if (!call)
|
|
3141
|
+
return;
|
|
3142
|
+
setJoinError(false);
|
|
3143
|
+
try {
|
|
3144
|
+
if (call.state.callingState !== videoClient.CallingState.JOINED) {
|
|
3145
|
+
await call.join();
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
catch (e) {
|
|
3149
|
+
onError?.(e);
|
|
3150
|
+
setJoinError(true);
|
|
3151
|
+
}
|
|
3152
|
+
}, [call, onError]);
|
|
3153
|
+
if (joinError) {
|
|
3154
|
+
return jsxRuntime.jsx(JoinError, { onJoin: handleJoin });
|
|
3155
|
+
}
|
|
3156
|
+
if (callingState === videoClient.CallingState.IDLE ||
|
|
3157
|
+
callingState === videoClient.CallingState.UNKNOWN) {
|
|
3158
|
+
return jsxRuntime.jsx(Lobby, { onJoin: handleJoin });
|
|
3159
|
+
}
|
|
3160
|
+
if (callingState === videoClient.CallingState.JOINING && !localParticipant) {
|
|
3161
|
+
return jsxRuntime.jsx(LoadingIndicator, { className: "str-video__embedded-loading" });
|
|
3162
|
+
}
|
|
3163
|
+
if (callingState === videoClient.CallingState.LEFT) {
|
|
3164
|
+
return jsxRuntime.jsx(CallFeedback, { onJoin: handleJoin });
|
|
3165
|
+
}
|
|
3166
|
+
return jsxRuntime.jsx(CallLayout, {});
|
|
3167
|
+
};
|
|
3168
|
+
|
|
3169
|
+
/**
|
|
3170
|
+
* Drop-in video call component that renders a lobby, active call,
|
|
3171
|
+
* and post-call feedback screen. Handles client and call setup internally.
|
|
3172
|
+
*/
|
|
3173
|
+
const EmbeddedCall = ({ children, ...props }) => (jsxRuntime.jsxs(EmbeddedClientProvider, { ...props, children: [jsxRuntime.jsx(CallStateRouter, {}), children] }));
|
|
3174
|
+
|
|
3175
|
+
const HostLayout = ({ isLive, isBackstageEnabled, onGoLive, onStopLive, }) => {
|
|
3176
|
+
const { t } = videoReactBindings.useI18n();
|
|
3177
|
+
const { useParticipantCount, useCallSession } = videoReactBindings.useCallStateHooks();
|
|
3178
|
+
const participantCount = useParticipantCount();
|
|
3179
|
+
const session = useCallSession();
|
|
3180
|
+
const { elapsed } = useCallDuration(session?.live_started_at);
|
|
3181
|
+
const { Component: LayoutComponent, props: layoutProps } = useLayout();
|
|
3182
|
+
const [showParticipants, setShowParticipants] = react.useState(false);
|
|
3183
|
+
const handleCloseParticipants = react.useCallback(() => {
|
|
3184
|
+
setShowParticipants(false);
|
|
3185
|
+
}, []);
|
|
3186
|
+
const handleToggleParticipants = react.useCallback(() => {
|
|
3187
|
+
setShowParticipants((prev) => !prev);
|
|
3188
|
+
}, []);
|
|
3189
|
+
const livestreamStatus = (jsxRuntime.jsxs("div", { className: "str-video__embedded-livestream-duration", "aria-live": "polite", "aria-atomic": "true", children: [jsxRuntime.jsx("span", { className: isLive
|
|
3190
|
+
? 'str-video__embedded-livestream-duration__live-badge'
|
|
3191
|
+
: 'str-video__embedded-livestream-duration__backstage-badge', children: isLive ? t('Live') : t('Backstage') }), jsxRuntime.jsx(ViewersCount, { count: participantCount }), isLive && elapsed && (jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__elapsed", children: elapsed }))] }));
|
|
3192
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call str-video__embedded-livestream", children: [jsxRuntime.jsx(ConnectionNotification, {}), jsxRuntime.jsx(PermissionRequests, {}), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-header", children: [livestreamStatus, jsxRuntime.jsx(CancelCallConfirmButton, {})] }), jsxRuntime.jsxs("div", { className: "str-video__embedded-layout", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-layout__stage", children: jsxRuntime.jsx(LayoutComponent, { ...layoutProps }) }), jsxRuntime.jsx("div", { className: clsx('str-video__embedded-sidebar', showParticipants && 'str-video__embedded-sidebar--open'), children: showParticipants && (jsxRuntime.jsx("div", { className: "str-video__embedded-participants", children: jsxRuntime.jsx(CallParticipantsList, { onClose: handleCloseParticipants }) })) })] }), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-controls str-video__call-controls", children: [jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--options", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-mobile", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: livestreamStatus })] }), jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--media", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], hasPermissionsOnly: true, children: jsxRuntime.jsx(MicCaptureErrorNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, { Menu: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DeviceSelectorAudioOutput, { visualType: "list", title: t('Speaker') }), jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list", title: t('Microphone') })] }), menuPlacement: "top" }) }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], hasPermissionsOnly: true, children: jsxRuntime.jsx(ToggleVideoPublishingButton, { Menu: jsxRuntime.jsx(CameraMenuWithBlur, {}), menuPlacement: "top" }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SCREENSHARE], children: jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(ScreenShareButton, {}) }) }), jsxRuntime.jsx(RecordCallConfirmationButton, {}), isBackstageEnabled && (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.UPDATE_CALL], children: isLive ? (jsxRuntime.jsx(WithTooltip, { title: t('End Stream'), children: jsxRuntime.jsxs("button", { type: "button", className: "str-video__embedded-end-stream-button", onClick: onStopLive, children: [jsxRuntime.jsx(Icon, { icon: "call-end" }), jsxRuntime.jsx("span", { children: t('Stop Live') })] }) })) : (jsxRuntime.jsx(WithTooltip, { title: t('Start Stream'), children: jsxRuntime.jsxs("button", { type: "button", className: "str-video__embedded-go-live-button", onClick: onGoLive, children: [jsxRuntime.jsx(Icon, { icon: "streaming" }), jsxRuntime.jsx("span", { children: t('Go Live') })] }) })) })), jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(CancelCallConfirmButton, {}) })] }), jsxRuntime.jsx("div", { className: "str-video__call-controls--group str-video__call-controls--sidebar", children: jsxRuntime.jsx(WithTooltip, { title: t('Participants'), children: jsxRuntime.jsx(CompositeButton, { active: showParticipants, "aria-label": t('Participants'), "aria-pressed": showParticipants, onClick: handleToggleParticipants, children: jsxRuntime.jsx(Icon, { icon: "participants" }) }) }) })] })] }));
|
|
3193
|
+
};
|
|
3194
|
+
|
|
3195
|
+
const HostStateRouter = () => {
|
|
3196
|
+
const call = videoReactBindings.useCall();
|
|
3197
|
+
const { t } = videoReactBindings.useI18n();
|
|
3198
|
+
const { onError } = useEmbeddedConfiguration();
|
|
3199
|
+
const { useCallCallingState, useIsCallLive, useCallSettings, useLocalParticipant, } = videoReactBindings.useCallStateHooks();
|
|
3200
|
+
const callingState = useCallCallingState();
|
|
3201
|
+
const isLive = useIsCallLive();
|
|
3202
|
+
const localParticipant = useLocalParticipant();
|
|
3203
|
+
const settings = useCallSettings();
|
|
3204
|
+
const isBackstageEnabled = settings?.backstage?.enabled ?? true;
|
|
3205
|
+
const [joinError, setJoinError] = react.useState(false);
|
|
3206
|
+
const handleJoin = react.useCallback(async () => {
|
|
3207
|
+
if (!call)
|
|
3208
|
+
return;
|
|
3209
|
+
setJoinError(false);
|
|
3210
|
+
try {
|
|
3211
|
+
if (callingState !== videoClient.CallingState.JOINED) {
|
|
3212
|
+
await call.join();
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
catch (err) {
|
|
3216
|
+
onError?.(err);
|
|
3217
|
+
setJoinError(true);
|
|
3218
|
+
}
|
|
3219
|
+
}, [call, onError, callingState]);
|
|
3220
|
+
const handleGoLive = react.useCallback(async () => {
|
|
3221
|
+
if (!call)
|
|
3222
|
+
return;
|
|
3223
|
+
try {
|
|
3224
|
+
await call.goLive();
|
|
3225
|
+
}
|
|
3226
|
+
catch (err) {
|
|
3227
|
+
onError?.(err);
|
|
3228
|
+
}
|
|
3229
|
+
}, [call, onError]);
|
|
3230
|
+
const handleStopLive = react.useCallback(async () => {
|
|
3231
|
+
if (!call)
|
|
3232
|
+
return;
|
|
3233
|
+
try {
|
|
3234
|
+
await call.stopLive();
|
|
3235
|
+
}
|
|
3236
|
+
catch (err) {
|
|
3237
|
+
onError?.(err);
|
|
3238
|
+
}
|
|
3239
|
+
}, [call, onError]);
|
|
3240
|
+
if (joinError) {
|
|
3241
|
+
return jsxRuntime.jsx(JoinError, { onJoin: handleJoin });
|
|
3242
|
+
}
|
|
3243
|
+
if (callingState === videoClient.CallingState.IDLE ||
|
|
3244
|
+
callingState === videoClient.CallingState.UNKNOWN) {
|
|
3245
|
+
return (jsxRuntime.jsx(Lobby, { onJoin: handleJoin, title: isBackstageEnabled
|
|
3246
|
+
? t('Prepare your livestream')
|
|
3247
|
+
: t('Ready to go live'), joinLabel: isBackstageEnabled ? t('Enter Backstage') : t('Go Live') }));
|
|
3248
|
+
}
|
|
3249
|
+
if (callingState === videoClient.CallingState.JOINING && !localParticipant) {
|
|
3250
|
+
return jsxRuntime.jsx(LoadingIndicator, { className: "str-video__embedded-loading" });
|
|
3251
|
+
}
|
|
3252
|
+
if (callingState === videoClient.CallingState.LEFT) {
|
|
3253
|
+
return jsxRuntime.jsx(CallFeedback, { onJoin: handleJoin });
|
|
3254
|
+
}
|
|
3255
|
+
return (jsxRuntime.jsx(HostLayout, { isLive: isLive, isBackstageEnabled: isBackstageEnabled, onGoLive: handleGoLive, onStopLive: handleStopLive }));
|
|
3256
|
+
};
|
|
3257
|
+
|
|
3258
|
+
const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
|
|
3259
|
+
if (!startsAt)
|
|
3260
|
+
return false;
|
|
3261
|
+
const now = Date.now();
|
|
3262
|
+
const earliestJoin = +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
|
|
3263
|
+
return now >= earliestJoin && now < +startsAt;
|
|
3264
|
+
};
|
|
3265
|
+
const ViewerLobby = ({ onJoin }) => {
|
|
3266
|
+
const { t } = videoReactBindings.useI18n();
|
|
3267
|
+
const { useCallStartsAt, useCallEndedAt, useHasPermissions, useParticipantCount, useIsCallLive, useCallSettings, } = videoReactBindings.useCallStateHooks();
|
|
3268
|
+
const startsAt = useCallStartsAt();
|
|
3269
|
+
const endedAt = useCallEndedAt();
|
|
3270
|
+
const canJoinEndedCall = useHasPermissions(videoClient.OwnCapability.JOIN_ENDED_CALL);
|
|
3271
|
+
const participantCount = useParticipantCount();
|
|
3272
|
+
const isLive = useIsCallLive();
|
|
3273
|
+
const settings = useCallSettings();
|
|
3274
|
+
const joinAheadTimeSeconds = settings?.backstage.join_ahead_time_seconds;
|
|
3275
|
+
const [autoJoin, setAutoJoin] = react.useState(false);
|
|
3276
|
+
const [startsAtPassed, setStartsAtPassed] = react.useState(() => !!startsAt && startsAt.getTime() < Date.now());
|
|
3277
|
+
const [canJoinEarly, setCanJoinEarly] = react.useState(() => checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
|
|
3278
|
+
const canJoin = (isLive || canJoinEarly) && (!endedAt || canJoinEndedCall);
|
|
3279
|
+
react.useEffect(() => {
|
|
3280
|
+
if (canJoin && autoJoin) {
|
|
3281
|
+
onJoin();
|
|
3282
|
+
}
|
|
3283
|
+
}, [canJoin, autoJoin, onJoin]);
|
|
3284
|
+
react.useEffect(() => {
|
|
3285
|
+
if (!canJoinEarly) {
|
|
3286
|
+
const handle = setInterval(() => {
|
|
3287
|
+
setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
|
|
3288
|
+
}, 1000);
|
|
3289
|
+
return () => clearInterval(handle);
|
|
3290
|
+
}
|
|
3291
|
+
}, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
|
|
3292
|
+
react.useEffect(() => {
|
|
3293
|
+
if (!startsAt || startsAtPassed)
|
|
3294
|
+
return;
|
|
3295
|
+
const check = () => {
|
|
3296
|
+
if (startsAt.getTime() < Date.now()) {
|
|
3297
|
+
setStartsAtPassed(true);
|
|
3298
|
+
}
|
|
3299
|
+
};
|
|
3300
|
+
const interval = setInterval(check, 1000);
|
|
3301
|
+
return () => clearInterval(interval);
|
|
3302
|
+
}, [startsAt, startsAtPassed]);
|
|
3303
|
+
const getStartsAtMessage = () => {
|
|
3304
|
+
if (!startsAt)
|
|
3305
|
+
return null;
|
|
3306
|
+
if (startsAtPassed) {
|
|
3307
|
+
return t('Livestream starts soon');
|
|
3308
|
+
}
|
|
3309
|
+
return t('Livestream starts at {{ time }}', {
|
|
3310
|
+
time: startsAt.toLocaleTimeString([], {
|
|
3311
|
+
hour: '2-digit',
|
|
3312
|
+
minute: '2-digit',
|
|
3313
|
+
}),
|
|
3314
|
+
});
|
|
3315
|
+
};
|
|
3316
|
+
return (jsxRuntime.jsx("div", { className: "str-video__embedded-viewer-lobby", children: jsxRuntime.jsxs("div", { className: "str-video__embedded-viewer-lobby__content", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-viewer-lobby__icon", children: jsxRuntime.jsx(Icon, { icon: "streaming" }) }), jsxRuntime.jsx("h2", { className: "str-video__embedded-viewer-lobby__title", children: canJoin
|
|
3317
|
+
? t('Stream is ready!')
|
|
3318
|
+
: t('Waiting for the livestream to start') }), !canJoin && getStartsAtMessage() && (jsxRuntime.jsx("p", { className: "str-video__embedded-viewer-lobby__starts-at", children: getStartsAtMessage() })), participantCount > 0 && jsxRuntime.jsx(ViewersCount, { count: participantCount }), jsxRuntime.jsx("div", { className: "str-video__embedded-viewer-lobby__actions", children: canJoin ? (jsxRuntime.jsx("button", { className: "str-video__button", onClick: onJoin, children: t('Join Stream') })) : (jsxRuntime.jsxs("label", { className: "str-video__embedded-viewer-lobby__auto-join", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoJoin, onChange: (e) => setAutoJoin(e.target.checked) }), jsxRuntime.jsx("span", { children: t('Join automatically when stream starts') })] })) })] }) }));
|
|
3319
|
+
};
|
|
3320
|
+
|
|
3321
|
+
const ViewerLayout = () => {
|
|
3322
|
+
useWakeLock();
|
|
3323
|
+
const { t } = videoReactBindings.useI18n();
|
|
3324
|
+
const { useParticipantCount, useCallSession } = videoReactBindings.useCallStateHooks();
|
|
3325
|
+
const participantCount = useParticipantCount();
|
|
3326
|
+
const session = useCallSession();
|
|
3327
|
+
const { elapsed } = useCallDuration(session?.live_started_at);
|
|
3328
|
+
const { Component: LayoutComponent, props: layoutProps } = useLayout();
|
|
3329
|
+
const [showParticipants, setShowParticipants] = react.useState(false);
|
|
3330
|
+
const handleCloseParticipants = react.useCallback(() => {
|
|
3331
|
+
setShowParticipants(false);
|
|
3332
|
+
}, []);
|
|
3333
|
+
const handleToggleParticipants = react.useCallback(() => {
|
|
3334
|
+
setShowParticipants((prev) => !prev);
|
|
3335
|
+
}, []);
|
|
3336
|
+
return (jsxRuntime.jsxs("div", { className: "str-video__embedded-call str-video__embedded-livestream", children: [jsxRuntime.jsx(ConnectionNotification, {}), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-header", children: [jsxRuntime.jsxs("div", { className: "str-video__embedded-livestream-duration", children: [jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__live-badge", children: t('Live') }), jsxRuntime.jsx(ViewersCount, { count: participantCount }), elapsed && (jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__elapsed", children: elapsed }))] }), jsxRuntime.jsx(CancelCallConfirmButton, {})] }), jsxRuntime.jsxs("div", { className: "str-video__embedded-layout", children: [jsxRuntime.jsx("div", { className: "str-video__embedded-layout__stage", children: jsxRuntime.jsx(LayoutComponent, { ...layoutProps }) }), jsxRuntime.jsx("div", { className: clsx('str-video__embedded-sidebar', showParticipants && 'str-video__embedded-sidebar--open'), children: showParticipants && (jsxRuntime.jsx("div", { className: "str-video__embedded-participants", children: jsxRuntime.jsx(CallParticipantsList, { onClose: handleCloseParticipants }) })) })] }), jsxRuntime.jsxs("div", { className: "str-video__embedded-call-controls str-video__call-controls", children: [jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--options", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-mobile", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsxs("div", { className: "str-video__embedded-livestream-duration", children: [jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__live-badge", children: t('Live') }), jsxRuntime.jsx(ViewersCount, { count: participantCount }), elapsed && (jsxRuntime.jsx("span", { className: "str-video__embedded-livestream-duration__elapsed", children: elapsed }))] }) })] }), jsxRuntime.jsxs("div", { className: "str-video__call-controls--group str-video__call-controls--media", children: [jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_AUDIO], hasPermissionsOnly: true, children: jsxRuntime.jsx(MicCaptureErrorNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, { Menu: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DeviceSelectorAudioOutput, { visualType: "list", title: t('Speaker') }), jsxRuntime.jsx(DeviceSelectorAudioInput, { visualType: "list", title: t('Microphone') })] }), menuPlacement: "top" }) }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.SEND_VIDEO], hasPermissionsOnly: true, children: jsxRuntime.jsx(ToggleVideoPublishingButton, { Menu: jsxRuntime.jsx(CameraMenuWithBlur, {}), menuPlacement: "top" }) }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(ReactionsButton, {}) }) }), jsxRuntime.jsx("div", { className: "str-video__embedded-desktop", children: jsxRuntime.jsx(CancelCallConfirmButton, {}) })] }), jsxRuntime.jsx("div", { className: "str-video__call-controls--group str-video__call-controls--sidebar", children: jsxRuntime.jsx(WithTooltip, { title: t('Participants'), children: jsxRuntime.jsx(CompositeButton, { active: showParticipants, "aria-label": t('Participants'), "aria-pressed": showParticipants, onClick: handleToggleParticipants, children: jsxRuntime.jsx(Icon, { icon: "participants" }) }) }) })] })] }));
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const ViewerStateRouter = () => {
|
|
3340
|
+
const call = videoReactBindings.useCall();
|
|
3341
|
+
const { onError } = useEmbeddedConfiguration();
|
|
3342
|
+
const { useCallCallingState, useCallEndedAt, useHasPermissions, useLocalParticipant, } = videoReactBindings.useCallStateHooks();
|
|
3343
|
+
const callingState = useCallCallingState();
|
|
3344
|
+
const localParticipant = useLocalParticipant();
|
|
3345
|
+
const endedAt = useCallEndedAt();
|
|
3346
|
+
const canJoinEndedCall = useHasPermissions(videoClient.OwnCapability.JOIN_ENDED_CALL);
|
|
3347
|
+
const isLivestreamPaused = useIsLivestreamPaused();
|
|
3348
|
+
const [joinError, setJoinError] = react.useState(false);
|
|
3349
|
+
const handleJoin = react.useCallback(async () => {
|
|
3350
|
+
if (!call)
|
|
3351
|
+
return;
|
|
3352
|
+
setJoinError(false);
|
|
3353
|
+
try {
|
|
3354
|
+
if (call.state.callingState !== videoClient.CallingState.JOINED) {
|
|
3355
|
+
await call.join();
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
catch (e) {
|
|
3359
|
+
onError?.(e);
|
|
3360
|
+
setJoinError(true);
|
|
3361
|
+
}
|
|
3362
|
+
}, [call, onError]);
|
|
3363
|
+
react.useEffect(() => {
|
|
3364
|
+
if (!call || callingState !== videoClient.CallingState.LEFT)
|
|
3365
|
+
return;
|
|
3366
|
+
return call.on('call.live_started', () => {
|
|
3367
|
+
call.get().catch((e) => {
|
|
3368
|
+
onError?.(e);
|
|
3369
|
+
});
|
|
3370
|
+
});
|
|
3371
|
+
}, [call, callingState, onError]);
|
|
3372
|
+
if (joinError) {
|
|
3373
|
+
return jsxRuntime.jsx(JoinError, { onJoin: handleJoin });
|
|
3374
|
+
}
|
|
3375
|
+
switch (callingState) {
|
|
3376
|
+
case videoClient.CallingState.IDLE:
|
|
3377
|
+
case videoClient.CallingState.UNKNOWN:
|
|
3378
|
+
return jsxRuntime.jsx(ViewerLobby, { onJoin: handleJoin });
|
|
3379
|
+
case videoClient.CallingState.JOINING:
|
|
3380
|
+
if (!localParticipant) {
|
|
3381
|
+
return jsxRuntime.jsx(LoadingIndicator, { className: "str-video__embedded-loading" });
|
|
3382
|
+
}
|
|
3383
|
+
break;
|
|
3384
|
+
case videoClient.CallingState.LEFT: {
|
|
3385
|
+
if (isLivestreamPaused) {
|
|
3386
|
+
return jsxRuntime.jsx(ViewerLobby, { onJoin: handleJoin });
|
|
3387
|
+
}
|
|
3388
|
+
return (jsxRuntime.jsx(CallFeedback, { onJoin: !endedAt || canJoinEndedCall ? handleJoin : undefined }));
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return jsxRuntime.jsx(ViewerLayout, {});
|
|
3392
|
+
};
|
|
3393
|
+
|
|
3394
|
+
const LivestreamUI = () => {
|
|
3395
|
+
const { useHasPermissions } = videoReactBindings.useCallStateHooks();
|
|
3396
|
+
const isHost = useHasPermissions(videoClient.OwnCapability.JOIN_BACKSTAGE);
|
|
3397
|
+
if (isHost) {
|
|
3398
|
+
return jsxRuntime.jsx(HostStateRouter, {});
|
|
3399
|
+
}
|
|
3400
|
+
return jsxRuntime.jsx(ViewerStateRouter, {});
|
|
3401
|
+
};
|
|
3402
|
+
|
|
3403
|
+
/**
|
|
3404
|
+
* Drop-in livestream component. Renders host or viewer UI based on permissions.
|
|
3405
|
+
*/
|
|
3406
|
+
const EmbeddedLivestream = ({ children, ...props }) => (jsxRuntime.jsxs(EmbeddedClientProvider, { ...props, children: [jsxRuntime.jsx(LivestreamUI, {}), children] }));
|
|
3407
|
+
|
|
3408
|
+
exports.EmbeddedCall = EmbeddedCall;
|
|
3409
|
+
exports.EmbeddedLivestream = EmbeddedLivestream;
|
|
3410
|
+
//# sourceMappingURL=embedded.cjs.js.map
|