@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/css/embedded.css +3630 -0
  3. package/dist/css/embedded.css.map +1 -0
  4. package/dist/css/styles.css +13 -2
  5. package/dist/css/styles.css.map +1 -1
  6. package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js +353 -0
  7. package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js.map +1 -0
  8. package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js +355 -0
  9. package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js.map +1 -0
  10. package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js +57 -0
  11. package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js.map +1 -0
  12. package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js +59 -0
  13. package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js.map +1 -0
  14. package/dist/embedded.cjs.js +3410 -0
  15. package/dist/embedded.cjs.js.map +1 -0
  16. package/dist/embedded.d.ts +1 -0
  17. package/dist/embedded.es.js +3407 -0
  18. package/dist/embedded.es.js.map +1 -0
  19. package/dist/index.cjs.js +67 -202
  20. package/dist/index.cjs.js.map +1 -1
  21. package/dist/index.es.js +69 -204
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/src/embedded/EmbeddedClientProvider.d.ts +21 -0
  24. package/dist/src/embedded/call/CallControls.d.ts +9 -0
  25. package/dist/src/embedded/call/CallHeader.d.ts +4 -0
  26. package/dist/src/embedded/call/CallLayout.d.ts +4 -0
  27. package/dist/src/embedded/call/CallStateRouter.d.ts +4 -0
  28. package/dist/src/embedded/call/EmbeddedCall.d.ts +6 -0
  29. package/dist/src/embedded/call/index.d.ts +1 -0
  30. package/dist/src/embedded/context/ConfigurationContext.d.ts +11 -0
  31. package/dist/src/embedded/context/index.d.ts +1 -0
  32. package/dist/src/embedded/hooks/index.d.ts +8 -0
  33. package/dist/src/embedded/hooks/useCallDuration.d.ts +7 -0
  34. package/dist/src/embedded/hooks/useEmbeddedClient.d.ts +22 -0
  35. package/dist/src/embedded/hooks/useInitializeCall.d.ts +11 -0
  36. package/dist/src/embedded/hooks/useInitializeVideoClient.d.ts +16 -0
  37. package/dist/src/embedded/hooks/useIsLivestreamPaused.d.ts +8 -0
  38. package/dist/src/embedded/hooks/useLayout.d.ts +9 -0
  39. package/dist/src/embedded/hooks/useNoiseCancellationLoader.d.ts +12 -0
  40. package/dist/src/embedded/hooks/useWakeLock.d.ts +5 -0
  41. package/dist/src/embedded/index.d.ts +3 -0
  42. package/dist/src/embedded/livestream/EmbeddedLivestream.d.ts +5 -0
  43. package/dist/src/embedded/livestream/LivestreamUI.d.ts +1 -0
  44. package/dist/src/embedded/livestream/host/HostLayout.d.ts +7 -0
  45. package/dist/src/embedded/livestream/host/HostStateRouter.d.ts +1 -0
  46. package/dist/src/embedded/livestream/index.d.ts +1 -0
  47. package/dist/src/embedded/livestream/viewer/ViewerLayout.d.ts +1 -0
  48. package/dist/src/embedded/livestream/viewer/ViewerLobby.d.ts +4 -0
  49. package/dist/src/embedded/livestream/viewer/ViewerStateRouter.d.ts +1 -0
  50. package/dist/src/embedded/shared/BlurToggleButton/BlurToggleButton.d.ts +2 -0
  51. package/dist/src/embedded/shared/CallFeedback/CallEndedScreen.d.ts +6 -0
  52. package/dist/src/embedded/shared/CallFeedback/CallFeedback.d.ts +4 -0
  53. package/dist/src/embedded/shared/CallFeedback/RatingScreen.d.ts +5 -0
  54. package/dist/src/embedded/shared/CallFeedback/StarRating.d.ts +6 -0
  55. package/dist/src/embedded/shared/CallFeedback/ThankYouScreen.d.ts +1 -0
  56. package/dist/src/embedded/shared/ConnectionNotification/ConnectionNotification.d.ts +1 -0
  57. package/dist/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.d.ts +1 -0
  58. package/dist/src/embedded/shared/JoinError/JoinError.d.ts +5 -0
  59. package/dist/src/embedded/shared/Lobby/DeviceControls.d.ts +5 -0
  60. package/dist/src/embedded/shared/Lobby/DisabledDeviceButton.d.ts +6 -0
  61. package/dist/src/embedded/shared/Lobby/Lobby.d.ts +10 -0
  62. package/dist/src/embedded/shared/Lobby/ToggleCameraButton.d.ts +1 -0
  63. package/dist/src/embedded/shared/Lobby/ToggleMicButton.d.ts +1 -0
  64. package/dist/src/embedded/shared/Lobby/VideoPreviewFallbacks.d.ts +2 -0
  65. package/dist/src/embedded/shared/ViewersCount/ViewersCount.d.ts +5 -0
  66. package/dist/src/embedded/shared/index.d.ts +7 -0
  67. package/dist/src/embedded/types.d.ts +65 -0
  68. package/dist/src/hooks/usePersistedDevicePreferences.d.ts +3 -12
  69. package/dist/src/translations/index.d.ts +42 -1
  70. package/embedded.ts +1 -0
  71. package/package.json +18 -4
  72. package/src/core/components/CallLayout/LivestreamLayout.tsx +53 -41
  73. package/src/embedded/EmbeddedClientProvider.tsx +125 -0
  74. package/src/embedded/call/CallControls.tsx +124 -0
  75. package/src/embedded/call/CallHeader.tsx +30 -0
  76. package/src/embedded/call/CallLayout.tsx +66 -0
  77. package/src/embedded/call/CallStateRouter.tsx +56 -0
  78. package/src/embedded/call/EmbeddedCall.tsx +14 -0
  79. package/src/embedded/call/index.ts +1 -0
  80. package/src/embedded/context/ConfigurationContext.tsx +36 -0
  81. package/src/embedded/context/index.ts +1 -0
  82. package/src/embedded/hooks/index.ts +8 -0
  83. package/src/embedded/hooks/useCallDuration.ts +40 -0
  84. package/src/embedded/hooks/useEmbeddedClient.ts +64 -0
  85. package/src/embedded/hooks/useInitializeCall.ts +51 -0
  86. package/src/embedded/hooks/useInitializeVideoClient.ts +118 -0
  87. package/src/embedded/hooks/useIsLivestreamPaused.ts +44 -0
  88. package/src/embedded/hooks/useLayout.ts +100 -0
  89. package/src/embedded/hooks/useNoiseCancellationLoader.ts +62 -0
  90. package/src/embedded/hooks/useWakeLock.ts +33 -0
  91. package/src/embedded/index.ts +12 -0
  92. package/src/embedded/livestream/EmbeddedLivestream.tsx +16 -0
  93. package/src/embedded/livestream/LivestreamUI.tsx +17 -0
  94. package/src/embedded/livestream/host/HostLayout.tsx +210 -0
  95. package/src/embedded/livestream/host/HostStateRouter.tsx +100 -0
  96. package/src/embedded/livestream/index.ts +1 -0
  97. package/src/embedded/livestream/viewer/ViewerLayout.tsx +160 -0
  98. package/src/embedded/livestream/viewer/ViewerLobby.tsx +135 -0
  99. package/src/embedded/livestream/viewer/ViewerStateRouter.tsx +82 -0
  100. package/src/embedded/shared/BlurToggleButton/BlurToggleButton.tsx +75 -0
  101. package/src/embedded/shared/CallFeedback/CallEndedScreen.tsx +55 -0
  102. package/src/embedded/shared/CallFeedback/CallFeedback.tsx +51 -0
  103. package/src/embedded/shared/CallFeedback/RatingScreen.tsx +47 -0
  104. package/src/embedded/shared/CallFeedback/StarRating.tsx +46 -0
  105. package/src/embedded/shared/CallFeedback/ThankYouScreen.tsx +19 -0
  106. package/src/embedded/shared/ConnectionNotification/ConnectionNotification.tsx +59 -0
  107. package/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.tsx +32 -0
  108. package/src/embedded/shared/JoinError/JoinError.tsx +27 -0
  109. package/src/embedded/shared/Lobby/DeviceControls.tsx +54 -0
  110. package/src/embedded/shared/Lobby/DisabledDeviceButton.tsx +21 -0
  111. package/src/embedded/shared/Lobby/Lobby.tsx +59 -0
  112. package/src/embedded/shared/Lobby/ToggleCameraButton.tsx +44 -0
  113. package/src/embedded/shared/Lobby/ToggleMicButton.tsx +48 -0
  114. package/src/embedded/shared/Lobby/VideoPreviewFallbacks.tsx +55 -0
  115. package/src/embedded/shared/ViewersCount/ViewersCount.tsx +18 -0
  116. package/src/embedded/shared/index.ts +7 -0
  117. package/src/embedded/types.ts +80 -0
  118. package/src/hooks/usePersistedDevicePreferences.ts +8 -307
  119. 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