@stream-io/video-react-sdk 0.3.41 → 0.3.42

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 (261) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/index.cjs.js +2737 -0
  4. package/dist/index.cjs.js.map +1 -0
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.es.js +2633 -0
  7. package/dist/index.es.js.map +1 -0
  8. package/dist/src/components/Button/CompositeButton.d.ts +1 -1
  9. package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +1 -1
  10. package/dist/src/hooks/useFloatingUIPreset.d.ts +1 -1
  11. package/index.ts +3 -3
  12. package/package.json +17 -15
  13. package/src/components/Permissions/PermissionRequests.tsx +1 -1
  14. package/dist/index.js +0 -18
  15. package/dist/index.js.map +0 -1
  16. package/dist/src/components/Avatar/Avatar.js +0 -24
  17. package/dist/src/components/Avatar/Avatar.js.map +0 -1
  18. package/dist/src/components/Avatar/index.js +0 -2
  19. package/dist/src/components/Avatar/index.js.map +0 -1
  20. package/dist/src/components/Button/CompositeButton.js +0 -13
  21. package/dist/src/components/Button/CompositeButton.js.map +0 -1
  22. package/dist/src/components/Button/CopyToClipboardButton.js +0 -54
  23. package/dist/src/components/Button/CopyToClipboardButton.js.map +0 -1
  24. package/dist/src/components/Button/IconButton.js +0 -26
  25. package/dist/src/components/Button/IconButton.js.map +0 -1
  26. package/dist/src/components/Button/TextButton.js +0 -17
  27. package/dist/src/components/Button/TextButton.js.map +0 -1
  28. package/dist/src/components/Button/index.js +0 -5
  29. package/dist/src/components/Button/index.js.map +0 -1
  30. package/dist/src/components/CallControls/AcceptCallButton.js +0 -27
  31. package/dist/src/components/CallControls/AcceptCallButton.js.map +0 -1
  32. package/dist/src/components/CallControls/CallControls.js +0 -5
  33. package/dist/src/components/CallControls/CallControls.js.map +0 -1
  34. package/dist/src/components/CallControls/CallStatsButton.js +0 -8
  35. package/dist/src/components/CallControls/CallStatsButton.js.map +0 -1
  36. package/dist/src/components/CallControls/CancelCallButton.js +0 -27
  37. package/dist/src/components/CallControls/CancelCallButton.js.map +0 -1
  38. package/dist/src/components/CallControls/ReactionsButton.js +0 -45
  39. package/dist/src/components/CallControls/ReactionsButton.js.map +0 -1
  40. package/dist/src/components/CallControls/RecordCallButton.js +0 -22
  41. package/dist/src/components/CallControls/RecordCallButton.js.map +0 -1
  42. package/dist/src/components/CallControls/ScreenShareButton.js +0 -15
  43. package/dist/src/components/CallControls/ScreenShareButton.js.map +0 -1
  44. package/dist/src/components/CallControls/ToggleAudioButton.js +0 -24
  45. package/dist/src/components/CallControls/ToggleAudioButton.js.map +0 -1
  46. package/dist/src/components/CallControls/ToggleAudioOutputButton.js +0 -10
  47. package/dist/src/components/CallControls/ToggleAudioOutputButton.js.map +0 -1
  48. package/dist/src/components/CallControls/ToggleVideoButton.js +0 -24
  49. package/dist/src/components/CallControls/ToggleVideoButton.js.map +0 -1
  50. package/dist/src/components/CallControls/index.js +0 -11
  51. package/dist/src/components/CallControls/index.js.map +0 -1
  52. package/dist/src/components/CallParticipantsList/BlockedUserListing.js +0 -18
  53. package/dist/src/components/CallParticipantsList/BlockedUserListing.js.map +0 -1
  54. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.js +0 -10
  55. package/dist/src/components/CallParticipantsList/CallParticipantListHeader.js.map +0 -1
  56. package/dist/src/components/CallParticipantsList/CallParticipantListing.js +0 -4
  57. package/dist/src/components/CallParticipantsList/CallParticipantListing.js.map +0 -1
  58. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js +0 -128
  59. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js.map +0 -1
  60. package/dist/src/components/CallParticipantsList/CallParticipantsList.js +0 -83
  61. package/dist/src/components/CallParticipantsList/CallParticipantsList.js.map +0 -1
  62. package/dist/src/components/CallParticipantsList/EmptyParticipantSearchList.js +0 -7
  63. package/dist/src/components/CallParticipantsList/EmptyParticipantSearchList.js.map +0 -1
  64. package/dist/src/components/CallParticipantsList/index.js +0 -4
  65. package/dist/src/components/CallParticipantsList/index.js.map +0 -1
  66. package/dist/src/components/CallPreview/CallPreview.js +0 -21
  67. package/dist/src/components/CallPreview/CallPreview.js.map +0 -1
  68. package/dist/src/components/CallPreview/index.js +0 -2
  69. package/dist/src/components/CallPreview/index.js.map +0 -1
  70. package/dist/src/components/CallRecordingList/CallRecordingList.js +0 -9
  71. package/dist/src/components/CallRecordingList/CallRecordingList.js.map +0 -1
  72. package/dist/src/components/CallRecordingList/CallRecordingListHeader.js +0 -6
  73. package/dist/src/components/CallRecordingList/CallRecordingListHeader.js.map +0 -1
  74. package/dist/src/components/CallRecordingList/CallRecordingListItem.js +0 -11
  75. package/dist/src/components/CallRecordingList/CallRecordingListItem.js.map +0 -1
  76. package/dist/src/components/CallRecordingList/EmptyCallRecordingListing.js +0 -5
  77. package/dist/src/components/CallRecordingList/EmptyCallRecordingListing.js.map +0 -1
  78. package/dist/src/components/CallRecordingList/LoadingCallRecordingListing.js +0 -7
  79. package/dist/src/components/CallRecordingList/LoadingCallRecordingListing.js.map +0 -1
  80. package/dist/src/components/CallRecordingList/index.js +0 -6
  81. package/dist/src/components/CallRecordingList/index.js.map +0 -1
  82. package/dist/src/components/CallStats/CallStats.js +0 -70
  83. package/dist/src/components/CallStats/CallStats.js.map +0 -1
  84. package/dist/src/components/CallStats/CallStatsLatencyChart.js +0 -39
  85. package/dist/src/components/CallStats/CallStatsLatencyChart.js.map +0 -1
  86. package/dist/src/components/CallStats/index.js +0 -3
  87. package/dist/src/components/CallStats/index.js.map +0 -1
  88. package/dist/src/components/Debug/DebugParticipantPublishQuality.js +0 -46
  89. package/dist/src/components/Debug/DebugParticipantPublishQuality.js.map +0 -1
  90. package/dist/src/components/Debug/DebugStatsView.js +0 -66
  91. package/dist/src/components/Debug/DebugStatsView.js.map +0 -1
  92. package/dist/src/components/Debug/useIsDebugMode.js +0 -18
  93. package/dist/src/components/Debug/useIsDebugMode.js.map +0 -1
  94. package/dist/src/components/DeviceSettings/DeviceSelector.js +0 -26
  95. package/dist/src/components/DeviceSettings/DeviceSelector.js.map +0 -1
  96. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.js +0 -20
  97. package/dist/src/components/DeviceSettings/DeviceSelectorAudio.js.map +0 -1
  98. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.js +0 -11
  99. package/dist/src/components/DeviceSettings/DeviceSelectorVideo.js.map +0 -1
  100. package/dist/src/components/DeviceSettings/DeviceSettings.js +0 -15
  101. package/dist/src/components/DeviceSettings/DeviceSettings.js.map +0 -1
  102. package/dist/src/components/DeviceSettings/index.js +0 -5
  103. package/dist/src/components/DeviceSettings/index.js.map +0 -1
  104. package/dist/src/components/Icon/Icon.js +0 -4
  105. package/dist/src/components/Icon/Icon.js.map +0 -1
  106. package/dist/src/components/Icon/index.js +0 -2
  107. package/dist/src/components/Icon/index.js.map +0 -1
  108. package/dist/src/components/LoadingIndicator/LoadingIndicator.js +0 -6
  109. package/dist/src/components/LoadingIndicator/LoadingIndicator.js.map +0 -1
  110. package/dist/src/components/LoadingIndicator/index.js +0 -2
  111. package/dist/src/components/LoadingIndicator/index.js.map +0 -1
  112. package/dist/src/components/Menu/GenericMenu.js +0 -20
  113. package/dist/src/components/Menu/GenericMenu.js.map +0 -1
  114. package/dist/src/components/Menu/MenuToggle.js +0 -40
  115. package/dist/src/components/Menu/MenuToggle.js.map +0 -1
  116. package/dist/src/components/Menu/index.js +0 -3
  117. package/dist/src/components/Menu/index.js.map +0 -1
  118. package/dist/src/components/Notification/Notification.js +0 -25
  119. package/dist/src/components/Notification/Notification.js.map +0 -1
  120. package/dist/src/components/Notification/PermissionNotification.js +0 -26
  121. package/dist/src/components/Notification/PermissionNotification.js.map +0 -1
  122. package/dist/src/components/Notification/SpeakingWhileMutedNotification.js +0 -50
  123. package/dist/src/components/Notification/SpeakingWhileMutedNotification.js.map +0 -1
  124. package/dist/src/components/Notification/index.js +0 -4
  125. package/dist/src/components/Notification/index.js.map +0 -1
  126. package/dist/src/components/Permissions/PermissionRequests.js +0 -122
  127. package/dist/src/components/Permissions/PermissionRequests.js.map +0 -1
  128. package/dist/src/components/Permissions/index.js +0 -2
  129. package/dist/src/components/Permissions/index.js.map +0 -1
  130. package/dist/src/components/Reaction/Reaction.js +0 -29
  131. package/dist/src/components/Reaction/Reaction.js.map +0 -1
  132. package/dist/src/components/Reaction/index.js +0 -2
  133. package/dist/src/components/Reaction/index.js.map +0 -1
  134. package/dist/src/components/RingingCall/RingingCall.js +0 -45
  135. package/dist/src/components/RingingCall/RingingCall.js.map +0 -1
  136. package/dist/src/components/RingingCall/RingingCallControls.js +0 -14
  137. package/dist/src/components/RingingCall/RingingCallControls.js.map +0 -1
  138. package/dist/src/components/RingingCall/index.js +0 -3
  139. package/dist/src/components/RingingCall/index.js.map +0 -1
  140. package/dist/src/components/Search/SearchInput.js +0 -34
  141. package/dist/src/components/Search/SearchInput.js.map +0 -1
  142. package/dist/src/components/Search/SearchResults.js +0 -12
  143. package/dist/src/components/Search/SearchResults.js.map +0 -1
  144. package/dist/src/components/Search/hooks/index.js +0 -2
  145. package/dist/src/components/Search/hooks/index.js.map +0 -1
  146. package/dist/src/components/Search/hooks/useSearch.js +0 -39
  147. package/dist/src/components/Search/hooks/useSearch.js.map +0 -1
  148. package/dist/src/components/Search/index.js +0 -3
  149. package/dist/src/components/Search/index.js.map +0 -1
  150. package/dist/src/components/StreamTheme/StreamTheme.js +0 -18
  151. package/dist/src/components/StreamTheme/StreamTheme.js.map +0 -1
  152. package/dist/src/components/StreamTheme/index.js +0 -2
  153. package/dist/src/components/StreamTheme/index.js.map +0 -1
  154. package/dist/src/components/Tooltip/Tooltip.js +0 -22
  155. package/dist/src/components/Tooltip/Tooltip.js.map +0 -1
  156. package/dist/src/components/Tooltip/WithTooltip.js +0 -23
  157. package/dist/src/components/Tooltip/WithTooltip.js.map +0 -1
  158. package/dist/src/components/Tooltip/hooks/index.js +0 -2
  159. package/dist/src/components/Tooltip/hooks/index.js.map +0 -1
  160. package/dist/src/components/Tooltip/hooks/useEnterLeaveHandlers.js +0 -14
  161. package/dist/src/components/Tooltip/hooks/useEnterLeaveHandlers.js.map +0 -1
  162. package/dist/src/components/Tooltip/index.js +0 -3
  163. package/dist/src/components/Tooltip/index.js.map +0 -1
  164. package/dist/src/components/Video/VideoPreview.js +0 -75
  165. package/dist/src/components/Video/VideoPreview.js.map +0 -1
  166. package/dist/src/components/Video/index.js +0 -5
  167. package/dist/src/components/Video/index.js.map +0 -1
  168. package/dist/src/components/index.js +0 -18
  169. package/dist/src/components/index.js.map +0 -1
  170. package/dist/src/core/components/Audio/Audio.js +0 -30
  171. package/dist/src/core/components/Audio/Audio.js.map +0 -1
  172. package/dist/src/core/components/Audio/ParticipantsAudio.js +0 -21
  173. package/dist/src/core/components/Audio/ParticipantsAudio.js.map +0 -1
  174. package/dist/src/core/components/Audio/index.js +0 -3
  175. package/dist/src/core/components/Audio/index.js.map +0 -1
  176. package/dist/src/core/components/CallLayout/LivestreamLayout.js +0 -89
  177. package/dist/src/core/components/CallLayout/LivestreamLayout.js.map +0 -1
  178. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js +0 -47
  179. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js.map +0 -1
  180. package/dist/src/core/components/CallLayout/SpeakerLayout.js +0 -71
  181. package/dist/src/core/components/CallLayout/SpeakerLayout.js.map +0 -1
  182. package/dist/src/core/components/CallLayout/hooks.js +0 -41
  183. package/dist/src/core/components/CallLayout/hooks.js.map +0 -1
  184. package/dist/src/core/components/CallLayout/index.js +0 -4
  185. package/dist/src/core/components/CallLayout/index.js.map +0 -1
  186. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js +0 -48
  187. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js.map +0 -1
  188. package/dist/src/core/components/ParticipantView/ParticipantView.js +0 -54
  189. package/dist/src/core/components/ParticipantView/ParticipantView.js.map +0 -1
  190. package/dist/src/core/components/ParticipantView/index.js +0 -3
  191. package/dist/src/core/components/ParticipantView/index.js.map +0 -1
  192. package/dist/src/core/components/StreamCall/StreamCall.js +0 -7
  193. package/dist/src/core/components/StreamCall/StreamCall.js.map +0 -1
  194. package/dist/src/core/components/StreamCall/index.js +0 -2
  195. package/dist/src/core/components/StreamCall/index.js.map +0 -1
  196. package/dist/src/core/components/StreamVideo/StreamVideo.js +0 -7
  197. package/dist/src/core/components/StreamVideo/StreamVideo.js.map +0 -1
  198. package/dist/src/core/components/StreamVideo/index.js +0 -2
  199. package/dist/src/core/components/StreamVideo/index.js.map +0 -1
  200. package/dist/src/core/components/Video/BaseVideo.js +0 -48
  201. package/dist/src/core/components/Video/BaseVideo.js.map +0 -1
  202. package/dist/src/core/components/Video/DefaultVideoPlaceholder.js +0 -9
  203. package/dist/src/core/components/Video/DefaultVideoPlaceholder.js.map +0 -1
  204. package/dist/src/core/components/Video/Video.js +0 -82
  205. package/dist/src/core/components/Video/Video.js.map +0 -1
  206. package/dist/src/core/components/Video/index.js +0 -3
  207. package/dist/src/core/components/Video/index.js.map +0 -1
  208. package/dist/src/core/components/index.js +0 -7
  209. package/dist/src/core/components/index.js.map +0 -1
  210. package/dist/src/core/contexts/MediaDevicesContext.js +0 -178
  211. package/dist/src/core/contexts/MediaDevicesContext.js.map +0 -1
  212. package/dist/src/core/contexts/index.js +0 -2
  213. package/dist/src/core/contexts/index.js.map +0 -1
  214. package/dist/src/core/hooks/index.js +0 -5
  215. package/dist/src/core/hooks/index.js.map +0 -1
  216. package/dist/src/core/hooks/useAudioPublisher.js +0 -114
  217. package/dist/src/core/hooks/useAudioPublisher.js.map +0 -1
  218. package/dist/src/core/hooks/useCalculateHardLimit.js +0 -56
  219. package/dist/src/core/hooks/useCalculateHardLimit.js.map +0 -1
  220. package/dist/src/core/hooks/useDevices.js +0 -172
  221. package/dist/src/core/hooks/useDevices.js.map +0 -1
  222. package/dist/src/core/hooks/useTrackElementVisibility.js +0 -15
  223. package/dist/src/core/hooks/useTrackElementVisibility.js.map +0 -1
  224. package/dist/src/core/hooks/useVideoPublisher.js +0 -139
  225. package/dist/src/core/hooks/useVideoPublisher.js.map +0 -1
  226. package/dist/src/core/index.js +0 -4
  227. package/dist/src/core/index.js.map +0 -1
  228. package/dist/src/hooks/index.js +0 -8
  229. package/dist/src/hooks/index.js.map +0 -1
  230. package/dist/src/hooks/useFloatingUIPreset.js +0 -30
  231. package/dist/src/hooks/useFloatingUIPreset.js.map +0 -1
  232. package/dist/src/hooks/useRequestPermission.js +0 -46
  233. package/dist/src/hooks/useRequestPermission.js.map +0 -1
  234. package/dist/src/hooks/useScrollPosition.js +0 -63
  235. package/dist/src/hooks/useScrollPosition.js.map +0 -1
  236. package/dist/src/hooks/useToggleAudioMuteState.js +0 -34
  237. package/dist/src/hooks/useToggleAudioMuteState.js.map +0 -1
  238. package/dist/src/hooks/useToggleCallRecording.js +0 -44
  239. package/dist/src/hooks/useToggleCallRecording.js.map +0 -1
  240. package/dist/src/hooks/useToggleScreenShare.js +0 -38
  241. package/dist/src/hooks/useToggleScreenShare.js.map +0 -1
  242. package/dist/src/hooks/useToggleVideoMuteState.js +0 -34
  243. package/dist/src/hooks/useToggleVideoMuteState.js.map +0 -1
  244. package/dist/src/translations/en.json +0 -73
  245. package/dist/src/translations/index.js +0 -3
  246. package/dist/src/translations/index.js.map +0 -1
  247. package/dist/src/types/components.js +0 -2
  248. package/dist/src/types/components.js.map +0 -1
  249. package/dist/src/types/index.js +0 -2
  250. package/dist/src/types/index.js.map +0 -1
  251. package/dist/src/utilities/applyElementToRef.js +0 -8
  252. package/dist/src/utilities/applyElementToRef.js.map +0 -1
  253. package/dist/src/utilities/chunk.js +0 -5
  254. package/dist/src/utilities/chunk.js.map +0 -1
  255. package/dist/src/utilities/index.js +0 -4
  256. package/dist/src/utilities/index.js.map +0 -1
  257. package/dist/src/utilities/isComponentType.js +0 -7
  258. package/dist/src/utilities/isComponentType.js.map +0 -1
  259. package/dist/version.d.ts +0 -1
  260. package/dist/version.js +0 -2
  261. package/dist/version.js.map +0 -1
@@ -0,0 +1,2737 @@
1
+ 'use strict';
2
+
3
+ var videoClient = require('@stream-io/video-client');
4
+ var videoReactBindings = require('@stream-io/video-react-bindings');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('react');
7
+ var clsx = require('clsx');
8
+ var rxjs = require('rxjs');
9
+ var operators = require('rxjs/operators');
10
+ var react$1 = require('@floating-ui/react');
11
+ var line = require('@nivo/line');
12
+
13
+ const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
14
+ const call = videoReactBindings.useCall();
15
+ const [audioElement, setAudioElement] = react.useState(null);
16
+ const { userId, sessionId } = participant;
17
+ react.useEffect(() => {
18
+ if (!call || !audioElement)
19
+ return;
20
+ const cleanup = call.bindAudioElement(audioElement, sessionId, trackType);
21
+ return () => {
22
+ cleanup?.();
23
+ };
24
+ }, [call, sessionId, audioElement, trackType]);
25
+ return (jsxRuntime.jsx("audio", { autoPlay: true, ...rest, ref: setAudioElement, "data-user-id": userId, "data-session-id": sessionId, "data-track-type": trackType }));
26
+ };
27
+
28
+ const ParticipantsAudio = (props) => {
29
+ const { participants, audioProps } = props;
30
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: participants.map((participant) => {
31
+ if (participant.isLocalParticipant)
32
+ return null;
33
+ const hasAudio = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
34
+ const hasScreenShareAudio = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
35
+ if (hasAudio && participant.audioStream) {
36
+ return (react.createElement(Audio, { ...audioProps, trackType: "audioTrack", participant: participant, key: participant.sessionId }));
37
+ }
38
+ if (hasScreenShareAudio && participant.screenShareAudioStream) {
39
+ return (react.createElement(Audio, { ...audioProps, trackType: "screenShareAudioTrack", participant: participant, key: participant.sessionId }));
40
+ }
41
+ return null;
42
+ }) }));
43
+ };
44
+
45
+ const isComponentType = (elementOrComponent) => {
46
+ return elementOrComponent === null
47
+ ? false
48
+ : !react.isValidElement(elementOrComponent);
49
+ };
50
+
51
+ const chunk = (array, size) => {
52
+ const chunkCount = Math.ceil(array.length / size);
53
+ return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
54
+ };
55
+
56
+ const applyElementToRef = (ref, element) => {
57
+ if (!ref)
58
+ return;
59
+ if (typeof ref === 'function')
60
+ return ref(element);
61
+ ref.current = element;
62
+ };
63
+
64
+ /**
65
+ * @description Extends video element with `stream` property
66
+ * (`srcObject`) to reactively handle stream changes
67
+ */
68
+ const BaseVideo = react.forwardRef(({ stream, ...rest }, ref) => {
69
+ const [videoElement, setVideoElement] = react.useState(null);
70
+ react.useEffect(() => {
71
+ if (!videoElement || !stream)
72
+ return;
73
+ if (stream === videoElement.srcObject)
74
+ return;
75
+ videoElement.srcObject = stream;
76
+ if (videoClient.Browsers.isSafari() || videoClient.Browsers.isFirefox()) {
77
+ // Firefox and Safari have some timing issue
78
+ setTimeout(() => {
79
+ videoElement.srcObject = stream;
80
+ videoElement.play().catch((e) => {
81
+ console.error(`Failed to play stream`, e);
82
+ });
83
+ }, 0);
84
+ }
85
+ return () => {
86
+ videoElement.pause();
87
+ videoElement.srcObject = null;
88
+ };
89
+ }, [stream, videoElement]);
90
+ return (jsxRuntime.jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
91
+ applyElementToRef(ref, element);
92
+ setVideoElement(element);
93
+ } }));
94
+ });
95
+
96
+ const DefaultVideoPlaceholder = react.forwardRef(({ participant, style }, ref) => {
97
+ const [error, setError] = react.useState(false);
98
+ const name = participant.name || participant.userId;
99
+ return (jsxRuntime.jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
100
+ (name ? (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: jsxRuntime.jsx("div", { children: name[0] }) })) : (jsxRuntime.jsx("div", { children: "Video is disabled" }))), participant.image && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "video-placeholder", className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
101
+ });
102
+
103
+ const Video$1 = ({ trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
104
+ const { sessionId, videoStream, screenShareStream, publishedTracks, viewportVisibilityState, isLocalParticipant, userId, } = participant;
105
+ const call = videoReactBindings.useCall();
106
+ const [videoElement, setVideoElement] = react.useState(null);
107
+ // start with true, will flip once the video starts playing
108
+ const [isVideoPaused, setIsVideoPaused] = react.useState(true);
109
+ const [isWideMode, setIsWideMode] = react.useState(true);
110
+ const stream = trackType === 'videoTrack'
111
+ ? videoStream
112
+ : trackType === 'screenShareTrack'
113
+ ? screenShareStream
114
+ : undefined;
115
+ react.useLayoutEffect(() => {
116
+ if (!call || !videoElement || trackType === 'none')
117
+ return;
118
+ const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
119
+ return () => {
120
+ cleanup?.();
121
+ };
122
+ }, [call, trackType, sessionId, videoElement]);
123
+ react.useEffect(() => {
124
+ if (!stream || !videoElement)
125
+ return;
126
+ const [track] = stream.getVideoTracks();
127
+ if (!track)
128
+ return;
129
+ const handlePlayPause = () => {
130
+ setIsVideoPaused(videoElement.paused);
131
+ const { width = 0, height = 0 } = track.getSettings();
132
+ setIsWideMode(width >= height);
133
+ };
134
+ videoElement.addEventListener('play', handlePlayPause);
135
+ videoElement.addEventListener('pause', handlePlayPause);
136
+ track.addEventListener('unmute', handlePlayPause);
137
+ return () => {
138
+ videoElement.removeEventListener('play', handlePlayPause);
139
+ videoElement.removeEventListener('pause', handlePlayPause);
140
+ track.removeEventListener('unmute', handlePlayPause);
141
+ };
142
+ }, [stream, videoElement]);
143
+ if (!call)
144
+ return null;
145
+ const isPublishingTrack = trackType === 'videoTrack'
146
+ ? publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO)
147
+ : trackType === 'screenShareTrack'
148
+ ? publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE)
149
+ : false;
150
+ const isInvisible = trackType === 'none' ||
151
+ viewportVisibilityState?.[trackType] === videoClient.VisibilityState.INVISIBLE;
152
+ const hasNoVideoOrInvisible = !isPublishingTrack || isInvisible;
153
+ const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
154
+ const isScreenShareTrack = trackType === 'screenShareTrack';
155
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!hasNoVideoOrInvisible && (jsxRuntime.jsx("video", { ...rest, className: clsx(className, 'str-video__video', {
156
+ 'str-video__video--not-playing': isVideoPaused,
157
+ 'str-video__video--tall': !isWideMode,
158
+ 'str-video__video--mirror': mirrorVideo,
159
+ 'str-video__video--screen-share': isScreenShareTrack,
160
+ }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
161
+ setVideoElement(element);
162
+ refs?.setVideoElement?.(element);
163
+ } })), (hasNoVideoOrInvisible || isVideoPaused) && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
164
+ };
165
+
166
+ const useHasBrowserPermissions = (permissionName) => {
167
+ const [canSubscribe, enableSubscription] = react.useState(false);
168
+ react.useEffect(() => {
169
+ let permissionState;
170
+ const handlePermissionChange = (e) => {
171
+ const { state } = e.target;
172
+ enableSubscription(state === 'granted');
173
+ };
174
+ const checkPermissions = async () => {
175
+ try {
176
+ permissionState = await navigator.permissions.query({
177
+ name: permissionName,
178
+ });
179
+ permissionState.addEventListener('change', handlePermissionChange);
180
+ enableSubscription(permissionState.state === 'granted');
181
+ }
182
+ catch (e) {
183
+ // permission does not exist - cannot be queried
184
+ // an example would be Firefox - camera, neither microphone perms can be queried
185
+ enableSubscription(true);
186
+ }
187
+ };
188
+ checkPermissions();
189
+ return () => {
190
+ permissionState?.removeEventListener('change', handlePermissionChange);
191
+ };
192
+ }, [permissionName]);
193
+ return canSubscribe;
194
+ };
195
+ /**
196
+ * Observes changes in connected devices and maintains an up-to-date array of connected MediaDeviceInfo objects.
197
+ * @param observeDevices
198
+ * @category Device Management
199
+ */
200
+ const useDevices = (observeDevices) => {
201
+ const [devices, setDevices] = react.useState([]);
202
+ react.useEffect(() => {
203
+ const subscription = observeDevices().subscribe(setDevices);
204
+ return () => {
205
+ subscription.unsubscribe();
206
+ };
207
+ }, [observeDevices]);
208
+ return devices;
209
+ };
210
+ /**
211
+ * Observes changes and maintains an array of connected video input devices
212
+ * @category Device Management
213
+ */
214
+ const useVideoDevices = () => useDevices(videoClient.getVideoDevices);
215
+ /**
216
+ * Observes changes and maintains an array of connected audio input devices
217
+ * @category Device Management
218
+ */
219
+ const useAudioInputDevices = () => useDevices(videoClient.getAudioDevices);
220
+ /**
221
+ * Observes changes and maintains an array of connected audio output devices
222
+ * @category Device Management
223
+ */
224
+ const useAudioOutputDevices = () => useDevices(videoClient.getAudioOutputDevices);
225
+ /**
226
+ * Verifies that newly selected device id exists among the registered devices.
227
+ * If the selected device id is not found among existing devices, switches to the default device.
228
+ * The media devices are observed only if a given permission ('camera' resp. 'microphone') is granted in browser.
229
+ * Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
230
+ * This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
231
+ * @param canObserve
232
+ * @param devices$
233
+ * @param switchToDefaultDevice
234
+ * @param selectedDeviceId
235
+ * @category Device Management
236
+ */
237
+ const useDeviceFallback = (canObserve, devices$, switchToDefaultDevice, selectedDeviceId) => {
238
+ react.useEffect(() => {
239
+ if (!canObserve)
240
+ return;
241
+ const validateDeviceId = devices$.pipe().subscribe((devices) => {
242
+ const deviceFound = devices.find((device) => device.deviceId === selectedDeviceId);
243
+ if (!deviceFound)
244
+ switchToDefaultDevice();
245
+ });
246
+ return () => {
247
+ validateDeviceId.unsubscribe();
248
+ };
249
+ }, [canObserve, devices$, selectedDeviceId, switchToDefaultDevice]);
250
+ };
251
+ /**
252
+ * Verifies that newly selected video device id exists among the registered devices.
253
+ * If the selected device id is not found among existing devices, switches to the default video device.
254
+ * The media devices are observed only if 'camera' permission is granted in browser.
255
+ * It is integrators responsibility to instruct users how to enable required permissions.
256
+ * Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
257
+ * This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
258
+ * @param switchToDefaultDevice
259
+ * @param canObserve
260
+ * @param selectedDeviceId
261
+ * @category Device Management
262
+ */
263
+ const useVideoDeviceFallback = (switchToDefaultDevice, canObserve, selectedDeviceId) => useDeviceFallback(canObserve, videoClient.getVideoDevices(), switchToDefaultDevice, selectedDeviceId);
264
+ /**
265
+ * Verifies that newly selected audio input device id exists among the registered devices.
266
+ * If the selected device id is not found among existing devices, switches to the default audio input device.
267
+ * The media devices are observed only if 'microphone' permission is granted in browser.
268
+ * It is integrators responsibility to instruct users how to enable required permissions.
269
+ * Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
270
+ * This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
271
+ * @param switchToDefaultDevice
272
+ * @param canObserve
273
+ * @param selectedDeviceId
274
+ * @category Device Management
275
+ */
276
+ const useAudioInputDeviceFallback = (switchToDefaultDevice, canObserve, selectedDeviceId) => useDeviceFallback(canObserve, videoClient.getAudioDevices(), switchToDefaultDevice, selectedDeviceId);
277
+ /**
278
+ * Verifies that newly selected audio output device id exists among the registered devices.
279
+ * If the selected device id is not found among existing devices, switches to the default audio output device.
280
+ * The media devices are observed only if 'microphone' permission is granted in browser.
281
+ * It is integrators responsibility to instruct users how to enable required permissions.
282
+ * Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
283
+ * This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
284
+ * @param switchToDefaultDevice
285
+ * @param canObserve
286
+ * @param selectedDeviceId
287
+ * @category Device Management
288
+ */
289
+ const useAudioOutputDeviceFallback = (switchToDefaultDevice, canObserve, selectedDeviceId) => useDeviceFallback(canObserve, videoClient.getAudioOutputDevices(), switchToDefaultDevice, selectedDeviceId);
290
+ /**
291
+ * Observes devices of certain kind are made unavailable and executes onDisconnect callback.
292
+ * @param observeDevices
293
+ * @param onDisconnect
294
+ * @category Device Management
295
+ */
296
+ const useOnUnavailableDevices = (observeDevices, onDisconnect) => {
297
+ react.useEffect(() => {
298
+ const subscription = observeDevices
299
+ .pipe(rxjs.pairwise())
300
+ .subscribe(([prev, current]) => {
301
+ if (prev.length > 0 && current.length === 0)
302
+ onDisconnect();
303
+ });
304
+ return () => subscription.unsubscribe();
305
+ }, [observeDevices, onDisconnect]);
306
+ };
307
+ /**
308
+ * Observes disconnect of all video devices and executes onDisconnect callback.
309
+ * @param onDisconnect
310
+ * @category Device Management
311
+ */
312
+ const useOnUnavailableVideoDevices = (onDisconnect) => useOnUnavailableDevices(videoClient.getVideoDevices(), onDisconnect);
313
+ /**
314
+ * Observes disconnect of all audio input devices and executes onDisconnect callback.
315
+ * @param onDisconnect
316
+ * @category Device Management
317
+ */
318
+ const useOnUnavailableAudioInputDevices = (onDisconnect) => useOnUnavailableDevices(videoClient.getAudioDevices(), onDisconnect);
319
+ /**
320
+ * Observes disconnect of all audio output devices and executes onDisconnect callback.
321
+ * @param onDisconnect
322
+ * @category Device Management
323
+ */
324
+ const useOnUnavailableAudioOutputDevices = (onDisconnect) => useOnUnavailableDevices(videoClient.getAudioOutputDevices(), onDisconnect);
325
+
326
+ /**
327
+ * @internal
328
+ * @category Device Management
329
+ */
330
+ const useAudioPublisher = ({ initialAudioMuted, audioDeviceId, }) => {
331
+ const call = videoReactBindings.useCall();
332
+ const { useCallState, useCallCallingState, useLocalParticipant } = videoReactBindings.useCallStateHooks();
333
+ const callState = useCallState();
334
+ const callingState = useCallCallingState();
335
+ const participant = useLocalParticipant();
336
+ const hasBrowserPermissionAudioInput = useHasBrowserPermissions('microphone');
337
+ const { localParticipant$ } = callState;
338
+ const isPublishingAudio = participant?.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
339
+ const publishAudioStream = react.useCallback(async () => {
340
+ if (!call)
341
+ return;
342
+ if (!call.permissionsContext.hasPermission(videoClient.OwnCapability.SEND_AUDIO)) {
343
+ throw new Error(`No permission to publish audio`);
344
+ }
345
+ try {
346
+ const audioStream = await videoClient.getAudioStream({
347
+ deviceId: audioDeviceId,
348
+ });
349
+ await call.publishAudioStream(audioStream);
350
+ }
351
+ catch (e) {
352
+ console.log('Failed to publish audio stream', e);
353
+ }
354
+ }, [audioDeviceId, call]);
355
+ const lastAudioDeviceId = react.useRef(audioDeviceId);
356
+ react.useEffect(() => {
357
+ if (callingState === videoClient.CallingState.JOINED &&
358
+ audioDeviceId !== lastAudioDeviceId.current) {
359
+ lastAudioDeviceId.current = audioDeviceId;
360
+ publishAudioStream().catch((e) => {
361
+ console.error('Failed to publish audio stream', e);
362
+ });
363
+ }
364
+ }, [audioDeviceId, callingState, publishAudioStream]);
365
+ const initialPublishRun = react.useRef(false);
366
+ react.useEffect(() => {
367
+ if (callingState === videoClient.CallingState.JOINED &&
368
+ !initialPublishRun.current &&
369
+ !initialAudioMuted) {
370
+ // automatic publishing should happen only when joining the call
371
+ // from the lobby, and the audio is not muted
372
+ publishAudioStream().catch((e) => {
373
+ console.error('Failed to publish audio stream', e);
374
+ });
375
+ initialPublishRun.current = true;
376
+ }
377
+ }, [callingState, initialAudioMuted, publishAudioStream]);
378
+ react.useEffect(() => {
379
+ if (!localParticipant$ || !hasBrowserPermissionAudioInput)
380
+ return;
381
+ const subscription = videoClient.watchForDisconnectedAudioDevice(localParticipant$.pipe(rxjs.map((p) => p?.audioDeviceId))).subscribe(async () => {
382
+ if (!call)
383
+ return;
384
+ call.setAudioDevice(undefined);
385
+ await call.stopPublish(videoClient.SfuModels.TrackType.AUDIO);
386
+ });
387
+ return () => {
388
+ subscription.unsubscribe();
389
+ };
390
+ }, [hasBrowserPermissionAudioInput, localParticipant$, call]);
391
+ react.useEffect(() => {
392
+ if (!participant?.audioStream || !call || !isPublishingAudio)
393
+ return;
394
+ const [track] = participant.audioStream.getAudioTracks();
395
+ const selectedAudioDeviceId = track.getSettings().deviceId;
396
+ const republishDefaultDevice = videoClient.watchForAddedDefaultAudioDevice().subscribe(async () => {
397
+ if (!(call &&
398
+ participant.audioStream &&
399
+ selectedAudioDeviceId === 'default'))
400
+ return;
401
+ // We need to stop the original track first in order
402
+ // we can retrieve the new default device stream
403
+ track.stop();
404
+ const audioStream = await videoClient.getAudioStream({
405
+ deviceId: 'default',
406
+ });
407
+ await call.publishAudioStream(audioStream);
408
+ });
409
+ const handleTrackEnded = async () => {
410
+ if (selectedAudioDeviceId === audioDeviceId) {
411
+ const audioStream = await videoClient.getAudioStream({
412
+ deviceId: audioDeviceId,
413
+ });
414
+ await call.publishAudioStream(audioStream);
415
+ }
416
+ };
417
+ track.addEventListener('ended', handleTrackEnded);
418
+ return () => {
419
+ track.removeEventListener('ended', handleTrackEnded);
420
+ republishDefaultDevice.unsubscribe();
421
+ };
422
+ }, [audioDeviceId, call, participant?.audioStream, isPublishingAudio]);
423
+ return publishAudioStream;
424
+ };
425
+
426
+ const useQueryParams = () => {
427
+ return react.useMemo(() => typeof window === 'undefined'
428
+ ? null
429
+ : new URLSearchParams(window.location.search), []);
430
+ };
431
+ /**
432
+ * Internal purpose hook. Enables certain development mode tools.
433
+ */
434
+ const useIsDebugMode = () => {
435
+ const params = useQueryParams();
436
+ return !!params?.get('debug');
437
+ };
438
+ const useDebugPreferredVideoCodec = () => {
439
+ const params = useQueryParams();
440
+ return params?.get('video_codec');
441
+ };
442
+
443
+ /**
444
+ * @internal
445
+ * @category Device Management
446
+ */
447
+ const useVideoPublisher = ({ initialVideoMuted, videoDeviceId, }) => {
448
+ const call = videoReactBindings.useCall();
449
+ const { useCallState, useCallCallingState, useLocalParticipant, useCallSettings, } = videoReactBindings.useCallStateHooks();
450
+ const callState = useCallState();
451
+ const callingState = useCallCallingState();
452
+ const participant = useLocalParticipant();
453
+ const hasBrowserPermissionVideoInput = useHasBrowserPermissions('camera');
454
+ const { localParticipant$ } = callState;
455
+ const preferredCodec = useDebugPreferredVideoCodec();
456
+ const isPublishingVideo = participant?.publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
457
+ const settings = useCallSettings();
458
+ const videoSettings = settings?.video;
459
+ const targetResolution = videoSettings?.target_resolution;
460
+ const publishVideoStream = react.useCallback(async () => {
461
+ if (!call)
462
+ return;
463
+ if (!call.permissionsContext.hasPermission(videoClient.OwnCapability.SEND_VIDEO)) {
464
+ throw new Error(`No permission to publish video`);
465
+ }
466
+ try {
467
+ const videoStream = await videoClient.getVideoStream({
468
+ deviceId: videoDeviceId,
469
+ width: targetResolution?.width,
470
+ height: targetResolution?.height,
471
+ facingMode: toFacingMode(videoSettings?.camera_facing),
472
+ });
473
+ await call.publishVideoStream(videoStream, { preferredCodec });
474
+ }
475
+ catch (e) {
476
+ console.log('Failed to publish video stream', e);
477
+ }
478
+ }, [
479
+ call,
480
+ preferredCodec,
481
+ targetResolution?.height,
482
+ targetResolution?.width,
483
+ videoDeviceId,
484
+ videoSettings?.camera_facing,
485
+ ]);
486
+ const lastVideoDeviceId = react.useRef(videoDeviceId);
487
+ react.useEffect(() => {
488
+ if (callingState === videoClient.CallingState.JOINED &&
489
+ videoDeviceId !== lastVideoDeviceId.current) {
490
+ lastVideoDeviceId.current = videoDeviceId;
491
+ publishVideoStream().catch((e) => {
492
+ console.error('Failed to publish video stream', e);
493
+ });
494
+ }
495
+ }, [publishVideoStream, videoDeviceId, callingState]);
496
+ const initialPublishRun = react.useRef(false);
497
+ react.useEffect(() => {
498
+ if (callingState === videoClient.CallingState.JOINED &&
499
+ !initialPublishRun.current &&
500
+ !initialVideoMuted) {
501
+ // automatic publishing should happen only when joining the call
502
+ // from the lobby, and the video is not muted
503
+ publishVideoStream().catch((e) => {
504
+ console.error('Failed to publish video stream', e);
505
+ });
506
+ initialPublishRun.current = true;
507
+ }
508
+ }, [callingState, initialVideoMuted, publishVideoStream]);
509
+ react.useEffect(() => {
510
+ if (!localParticipant$ || !hasBrowserPermissionVideoInput)
511
+ return;
512
+ const subscription = videoClient.watchForDisconnectedVideoDevice(localParticipant$.pipe(operators.map((p) => p?.videoDeviceId))).subscribe(async () => {
513
+ if (!call)
514
+ return;
515
+ call.setVideoDevice(undefined);
516
+ await call.stopPublish(videoClient.SfuModels.TrackType.VIDEO);
517
+ });
518
+ return () => {
519
+ subscription.unsubscribe();
520
+ };
521
+ }, [hasBrowserPermissionVideoInput, localParticipant$, call]);
522
+ react.useEffect(() => {
523
+ if (!participant?.videoStream || !call || !isPublishingVideo)
524
+ return;
525
+ const [track] = participant.videoStream.getVideoTracks();
526
+ const selectedVideoDeviceId = track.getSettings().deviceId;
527
+ const republishDefaultDevice = videoClient.watchForAddedDefaultVideoDevice().subscribe(async () => {
528
+ if (!(call &&
529
+ participant.videoStream &&
530
+ selectedVideoDeviceId === 'default'))
531
+ return;
532
+ // We need to stop the original track first in order
533
+ // we can retrieve the new default device stream
534
+ track.stop();
535
+ const videoStream = await videoClient.getVideoStream({
536
+ deviceId: 'default',
537
+ });
538
+ await call.publishVideoStream(videoStream);
539
+ });
540
+ const handleTrackEnded = async () => {
541
+ if (selectedVideoDeviceId === videoDeviceId) {
542
+ const videoStream = await videoClient.getVideoStream({
543
+ deviceId: videoDeviceId,
544
+ });
545
+ await call.publishVideoStream(videoStream);
546
+ }
547
+ };
548
+ track.addEventListener('ended', handleTrackEnded);
549
+ return () => {
550
+ track.removeEventListener('ended', handleTrackEnded);
551
+ republishDefaultDevice.unsubscribe();
552
+ };
553
+ }, [videoDeviceId, call, participant?.videoStream, isPublishingVideo]);
554
+ return publishVideoStream;
555
+ };
556
+ const toFacingMode = (value) => {
557
+ switch (value) {
558
+ case videoClient.VideoSettingsCameraFacingEnum.FRONT:
559
+ return 'user';
560
+ case videoClient.VideoSettingsCameraFacingEnum.BACK:
561
+ return 'environment';
562
+ default:
563
+ return undefined;
564
+ }
565
+ };
566
+
567
+ const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
568
+ const call = videoReactBindings.useCall();
569
+ const manager = propsDynascaleManager ?? call?.dynascaleManager;
570
+ react.useEffect(() => {
571
+ if (!trackedElement || !manager || !call || trackType === 'none')
572
+ return;
573
+ const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
574
+ return () => {
575
+ unobserve();
576
+ };
577
+ }, [trackedElement, manager, call, sessionId, trackType]);
578
+ };
579
+
580
+ const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
581
+ const [error, setError] = react.useState(false);
582
+ 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 }))] }));
583
+ };
584
+ const AvatarFallback = ({ className, names, style, }) => {
585
+ 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]] }) }));
586
+ };
587
+
588
+ const useFloatingUIPreset = ({ placement, strategy, }) => {
589
+ const { refs, x, y, update, elements: { domReference, floating }, } = react$1.useFloating({
590
+ placement,
591
+ strategy,
592
+ middleware: [
593
+ react$1.offset(10),
594
+ react$1.shift(),
595
+ react$1.flip(),
596
+ react$1.size({
597
+ padding: 10,
598
+ apply: ({ availableHeight, elements }) => {
599
+ Object.assign(elements.floating.style, {
600
+ maxHeight: `${availableHeight}px`,
601
+ });
602
+ },
603
+ }),
604
+ ],
605
+ });
606
+ // handle window resizing
607
+ react.useEffect(() => {
608
+ if (!domReference || !floating)
609
+ return;
610
+ const cleanup = react$1.autoUpdate(domReference, floating, update);
611
+ return () => cleanup();
612
+ }, [domReference, floating, update]);
613
+ return { refs, x, y, domReference, floating, strategy };
614
+ };
615
+
616
+ const SCROLL_THRESHOLD = 10;
617
+ /**
618
+ * Hook which observes element's scroll position and returns text value based on the
619
+ * position of the scrollbar (`top`, `bottom`, `between` and `null` if no scrollbar is available)
620
+ */
621
+ const useVerticalScrollPosition = (scrollElement, threshold = SCROLL_THRESHOLD) => {
622
+ const [scrollPosition, setScrollPosition] = react.useState(null);
623
+ react.useEffect(() => {
624
+ if (!scrollElement)
625
+ return;
626
+ const scrollHandler = () => {
627
+ const element = scrollElement;
628
+ const hasVerticalScrollbar = element.scrollHeight > element.clientHeight;
629
+ if (!hasVerticalScrollbar)
630
+ return setScrollPosition(null);
631
+ const isAtTheTop = element.scrollTop <= threshold;
632
+ if (isAtTheTop)
633
+ return setScrollPosition('top');
634
+ const isAtTheBottom = Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight) <= threshold;
635
+ if (isAtTheBottom)
636
+ return setScrollPosition('bottom');
637
+ setScrollPosition('between');
638
+ };
639
+ const resizeObserver = new ResizeObserver(scrollHandler);
640
+ resizeObserver.observe(scrollElement);
641
+ scrollElement.addEventListener('scroll', scrollHandler);
642
+ return () => {
643
+ scrollElement.removeEventListener('scroll', scrollHandler);
644
+ resizeObserver.disconnect();
645
+ };
646
+ }, [scrollElement, threshold]);
647
+ return scrollPosition;
648
+ };
649
+ const useHorizontalScrollPosition = (scrollElement, threshold = SCROLL_THRESHOLD) => {
650
+ const [scrollPosition, setScrollPosition] = react.useState(null);
651
+ react.useEffect(() => {
652
+ if (!scrollElement)
653
+ return;
654
+ const scrollHandler = () => {
655
+ const element = scrollElement;
656
+ const hasHorizontalScrollbar = element.scrollWidth > element.clientWidth;
657
+ if (!hasHorizontalScrollbar)
658
+ return setScrollPosition(null);
659
+ const isAtTheStart = element.scrollLeft <= threshold;
660
+ if (isAtTheStart)
661
+ return setScrollPosition('start');
662
+ const isAtTheEnd = Math.abs(element.scrollWidth - element.scrollLeft - element.clientWidth) <= threshold;
663
+ if (isAtTheEnd)
664
+ return setScrollPosition('end');
665
+ setScrollPosition('between');
666
+ };
667
+ const resizeObserver = new ResizeObserver(scrollHandler);
668
+ resizeObserver.observe(scrollElement);
669
+ scrollElement.addEventListener('scroll', scrollHandler);
670
+ return () => {
671
+ scrollElement.removeEventListener('scroll', scrollHandler);
672
+ resizeObserver.disconnect();
673
+ };
674
+ }, [scrollElement, threshold]);
675
+ return scrollPosition;
676
+ };
677
+
678
+ const useRequestPermission = (permission) => {
679
+ const call = videoReactBindings.useCall();
680
+ const hasPermission = videoReactBindings.useHasPermissions(permission);
681
+ const [isAwaitingPermission, setIsAwaitingPermission] = react.useState(false); // TODO: load with possibly pending state
682
+ react.useEffect(() => {
683
+ const reset = () => setIsAwaitingPermission(false);
684
+ if (hasPermission)
685
+ reset();
686
+ }, [hasPermission]);
687
+ const requestPermission = react.useCallback(async () => {
688
+ if (hasPermission)
689
+ return true;
690
+ const canRequestPermission = !!call?.permissionsContext.canRequest(permission);
691
+ if (isAwaitingPermission || !canRequestPermission)
692
+ return false;
693
+ setIsAwaitingPermission(true);
694
+ try {
695
+ await call?.requestPermissions({
696
+ permissions: [permission],
697
+ });
698
+ }
699
+ catch (error) {
700
+ setIsAwaitingPermission(false);
701
+ throw new Error(`requestPermission failed: ${error}`);
702
+ }
703
+ return false;
704
+ }, [call, hasPermission, isAwaitingPermission, permission]);
705
+ return {
706
+ requestPermission,
707
+ hasPermission,
708
+ canRequestPermission: !!call?.permissionsContext.canRequest(permission),
709
+ isAwaitingPermission,
710
+ };
711
+ };
712
+
713
+ const useToggleAudioMuteState = () => {
714
+ const { publishAudioStream, stopPublishingAudio } = useMediaDevices();
715
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
716
+ const localParticipant = useLocalParticipant();
717
+ const { isAwaitingPermission, requestPermission } = useRequestPermission(videoClient.OwnCapability.SEND_AUDIO);
718
+ // to keep the toggle function as stable as possible
719
+ const isAudioMutedReference = react.useRef(false);
720
+ isAudioMutedReference.current = !localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
721
+ const toggleAudioMuteState = react.useCallback(async () => {
722
+ if (isAudioMutedReference.current) {
723
+ const canPublish = await requestPermission();
724
+ if (canPublish)
725
+ return publishAudioStream();
726
+ }
727
+ if (!isAudioMutedReference.current)
728
+ await stopPublishingAudio();
729
+ }, [publishAudioStream, requestPermission, stopPublishingAudio]);
730
+ return { toggleAudioMuteState, isAwaitingPermission };
731
+ };
732
+
733
+ const useToggleVideoMuteState = () => {
734
+ const { publishVideoStream, stopPublishingVideo } = useMediaDevices();
735
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
736
+ const localParticipant = useLocalParticipant();
737
+ const { isAwaitingPermission, requestPermission } = useRequestPermission(videoClient.OwnCapability.SEND_VIDEO);
738
+ // to keep the toggle function as stable as possible
739
+ const isVideoMutedReference = react.useRef(false);
740
+ isVideoMutedReference.current = !localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
741
+ const toggleVideoMuteState = react.useCallback(async () => {
742
+ if (isVideoMutedReference.current) {
743
+ const canPublish = await requestPermission();
744
+ if (canPublish)
745
+ return publishVideoStream();
746
+ }
747
+ if (!isVideoMutedReference.current)
748
+ await stopPublishingVideo();
749
+ }, [publishVideoStream, requestPermission, stopPublishingVideo]);
750
+ return { toggleVideoMuteState, isAwaitingPermission };
751
+ };
752
+
753
+ const useToggleScreenShare = () => {
754
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
755
+ const localParticipant = useLocalParticipant();
756
+ const call = videoReactBindings.useCall();
757
+ const isScreenSharingReference = react.useRef(false);
758
+ const { isAwaitingPermission, requestPermission } = useRequestPermission(videoClient.OwnCapability.SCREENSHARE);
759
+ const isScreenSharing = !!localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
760
+ isScreenSharingReference.current = isScreenSharing;
761
+ const toggleScreenShare = react.useCallback(async () => {
762
+ if (!isScreenSharingReference.current) {
763
+ const canPublish = await requestPermission();
764
+ if (!canPublish)
765
+ return;
766
+ const stream = await videoClient.getScreenShareStream().catch((e) => {
767
+ console.log(`Can't share screen: ${e}`);
768
+ });
769
+ if (stream) {
770
+ return call?.publishScreenShareStream(stream);
771
+ }
772
+ }
773
+ await call?.stopPublish(videoClient.SfuModels.TrackType.SCREEN_SHARE);
774
+ }, [call, requestPermission]);
775
+ return { toggleScreenShare, isAwaitingPermission, isScreenSharing };
776
+ };
777
+
778
+ const useToggleCallRecording = () => {
779
+ const call = videoReactBindings.useCall();
780
+ const { useIsCallRecordingInProgress } = videoReactBindings.useCallStateHooks();
781
+ const isCallRecordingInProgress = useIsCallRecordingInProgress();
782
+ const [isAwaitingResponse, setIsAwaitingResponse] = react.useState(false);
783
+ // TODO: add permissions
784
+ react.useEffect(() => {
785
+ // we wait until call.recording_started/stopped event to flips the
786
+ // `isCallRecordingInProgress` state variable.
787
+ // Once the flip happens, we remove the loading indicator
788
+ setIsAwaitingResponse((isAwaiting) => {
789
+ if (isAwaiting)
790
+ return false;
791
+ return isAwaiting;
792
+ });
793
+ }, [isCallRecordingInProgress]);
794
+ const toggleCallRecording = react.useCallback(async () => {
795
+ try {
796
+ setIsAwaitingResponse(true);
797
+ if (isCallRecordingInProgress) {
798
+ await call?.stopRecording();
799
+ }
800
+ else {
801
+ await call?.startRecording();
802
+ }
803
+ }
804
+ catch (e) {
805
+ console.error(`Failed start recording`, e);
806
+ }
807
+ }, [call, isCallRecordingInProgress]);
808
+ return { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress };
809
+ };
810
+
811
+ const MenuToggle = ({ ToggleButton, placement = 'top-start', strategy = 'absolute', children, }) => {
812
+ const [menuShown, setMenuShown] = react.useState(false);
813
+ const { floating, domReference, refs, x, y } = useFloatingUIPreset({
814
+ placement,
815
+ strategy,
816
+ });
817
+ react.useEffect(() => {
818
+ const handleClick = (event) => {
819
+ if (!floating && domReference?.contains(event.target)) {
820
+ setMenuShown(true);
821
+ }
822
+ else if (floating && !floating?.contains(event.target)) {
823
+ setMenuShown(false);
824
+ }
825
+ };
826
+ const handleKeyDown = (event) => {
827
+ if (event.key.toLowerCase() === 'escape' &&
828
+ !event.altKey &&
829
+ !event.ctrlKey) {
830
+ setMenuShown(false);
831
+ }
832
+ };
833
+ document?.addEventListener('click', handleClick, { capture: true });
834
+ document?.addEventListener('keydown', handleKeyDown);
835
+ return () => {
836
+ document?.removeEventListener('click', handleClick, { capture: true });
837
+ document?.removeEventListener('keydown', handleKeyDown);
838
+ };
839
+ }, [floating, domReference]);
840
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [menuShown && (jsxRuntime.jsx("div", { className: "str-video__menu-container", ref: refs.setFloating, style: {
841
+ position: strategy,
842
+ top: y ?? 0,
843
+ left: x ?? 0,
844
+ overflowY: 'auto',
845
+ }, children: children })), jsxRuntime.jsx(ToggleButton, { menuShown: menuShown, ref: refs.setReference })] }));
846
+ };
847
+
848
+ const GenericMenu = ({ children }) => {
849
+ return jsxRuntime.jsx("ul", { className: "str-video__generic-menu", children: children });
850
+ };
851
+ const GenericMenuButtonItem = ({ children, ...rest }) => {
852
+ return (jsxRuntime.jsx("li", { className: "str-video__generic-menu--item", children: jsxRuntime.jsx("button", { ...rest, children: children }) }));
853
+ };
854
+
855
+ const Icon = ({ icon }) => (jsxRuntime.jsx("span", { className: clsx.clsx('str-video__icon', icon && `str-video__icon--${icon}`) }));
856
+
857
+ const IconButton = react.forwardRef((props, ref) => {
858
+ const { icon, enabled, variant, onClick, className, ...rest } = props;
859
+ return (jsxRuntime.jsx("button", { className: clsx('str-video__call-controls__button', className, {
860
+ [`str-video__call-controls__button--variant-${variant}`]: variant,
861
+ 'str-video__call-controls__button--enabled': enabled,
862
+ }), onClick: (e) => {
863
+ e.preventDefault();
864
+ onClick?.(e);
865
+ }, ref: ref, ...rest, children: jsxRuntime.jsx(Icon, { icon: icon }) }));
866
+ });
867
+
868
+ const CompositeButton = react.forwardRef(({ caption, children, active, Menu, menuPlacement }, ref) => {
869
+ return (jsxRuntime.jsxs("div", { className: "str-video__composite-button", ref: ref, children: [jsxRuntime.jsxs("div", { className: clsx('str-video__composite-button__button-group', {
870
+ 'str-video__composite-button__button-group--active': active,
871
+ }), children: [children, Menu && (jsxRuntime.jsx(MenuToggle, { placement: menuPlacement, ToggleButton: ToggleMenuButton$2, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
872
+ });
873
+ const ToggleMenuButton$2 = react.forwardRef(({ menuShown }, ref) => (jsxRuntime.jsx(IconButton, { className: 'str-video__menu-toggle-button', icon: menuShown ? 'caret-down' : 'caret-up', title: "Toggle device menu", ref: ref })));
874
+
875
+ const Tooltip = ({ children, referenceElement, tooltipClassName, tooltipPlacement = 'top', visible = false, }) => {
876
+ const { refs, x, y, strategy } = useFloatingUIPreset({
877
+ placement: tooltipPlacement,
878
+ strategy: 'absolute',
879
+ });
880
+ react.useEffect(() => {
881
+ refs.setReference(referenceElement);
882
+ }, [referenceElement, refs]);
883
+ if (!visible)
884
+ return null;
885
+ return (jsxRuntime.jsx("div", { className: clsx('str-video__tooltip', tooltipClassName), ref: refs.setFloating, style: {
886
+ position: strategy,
887
+ top: y ?? 0,
888
+ left: x ?? 0,
889
+ overflowY: 'auto',
890
+ }, children: children }));
891
+ };
892
+
893
+ const useEnterLeaveHandlers = ({ onMouseEnter, onMouseLeave, } = {}) => {
894
+ const [tooltipVisible, setTooltipVisible] = react.useState(false);
895
+ const handleMouseEnter = react.useCallback((e) => {
896
+ setTooltipVisible(true);
897
+ onMouseEnter?.(e);
898
+ }, [onMouseEnter]);
899
+ const handleMouseLeave = react.useCallback((e) => {
900
+ setTooltipVisible(false);
901
+ onMouseLeave?.(e);
902
+ }, [onMouseLeave]);
903
+ return { handleMouseEnter, handleMouseLeave, tooltipVisible };
904
+ };
905
+
906
+ // todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
907
+ const WithTooltip = ({ title, tooltipClassName, tooltipPlacement, ...props }) => {
908
+ const { handleMouseEnter, handleMouseLeave, tooltipVisible } = useEnterLeaveHandlers();
909
+ const [tooltipAnchor, setTooltipAnchor] = react.useState(null);
910
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { referenceElement: tooltipAnchor, visible: tooltipVisible, tooltipClassName: tooltipClassName, tooltipPlacement: tooltipPlacement, children: title || '' }), jsxRuntime.jsx("div", { ref: setTooltipAnchor, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props })] }));
911
+ };
912
+
913
+ const CopyToClipboardButton = react.forwardRef(({ Button, className, copyValue, onClick, onError, onSuccess, ...restProps }, ref) => {
914
+ const handleClick = react.useCallback(async (event) => {
915
+ if (onClick)
916
+ onClick(event);
917
+ const value = typeof copyValue === 'function' ? copyValue() : copyValue;
918
+ try {
919
+ await navigator?.clipboard.writeText(value);
920
+ onSuccess?.(event.target);
921
+ }
922
+ catch (error) {
923
+ onError?.(event.target, error);
924
+ }
925
+ }, [copyValue, onClick, onError, onSuccess]);
926
+ const props = {
927
+ ...restProps,
928
+ ref: ref,
929
+ className: clsx('str-video__copy-to-clipboard-button', className),
930
+ onClick: handleClick,
931
+ };
932
+ return Button ? jsxRuntime.jsx(Button, { ...props }) : jsxRuntime.jsx("button", { ...props });
933
+ });
934
+ const CopyToClipboardButtonWithPopup = ({ dismissAfterMs = 1500, onErrorMessage = 'Failed to copy', onSuccessMessage = 'Copied to clipboard', popupClassName, popupPlacement, ...restProps }) => {
935
+ const [tooltipText, setTooltipText] = react.useState('');
936
+ const [tooltipAnchor, setTooltipAnchor] = react.useState(null);
937
+ const setTemporaryPopup = react.useCallback((popupText) => {
938
+ setTooltipText(popupText);
939
+ setTimeout(() => setTooltipText(''), dismissAfterMs);
940
+ }, [dismissAfterMs]);
941
+ const onSuccess = react.useCallback(() => setTemporaryPopup(onSuccessMessage), [onSuccessMessage, setTemporaryPopup]);
942
+ const onError = react.useCallback(() => setTemporaryPopup(onErrorMessage), [onErrorMessage, setTemporaryPopup]);
943
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { tooltipClassName: clsx('str-video__copy-to-clipboard-button__popup', popupClassName), tooltipPlacement: popupPlacement, referenceElement: tooltipAnchor, visible: !!tooltipText, children: tooltipText }), jsxRuntime.jsx(CopyToClipboardButton, { ...restProps, onError: onError, onSuccess: onSuccess, ref: setTooltipAnchor })] }));
944
+ };
945
+
946
+ const TextButton = ({ children, ...rest }) => {
947
+ return (jsxRuntime.jsx("button", { ...rest, className: "str-video__text-button", children: children }));
948
+ };
949
+
950
+ const AcceptCallButton = ({ disabled, onAccept, onClick, }) => {
951
+ const call = videoReactBindings.useCall();
952
+ const handleClick = react.useCallback(async (e) => {
953
+ if (onClick) {
954
+ onClick(e);
955
+ }
956
+ else if (call) {
957
+ await call.join();
958
+ onAccept?.();
959
+ }
960
+ }, [onClick, onAccept, call]);
961
+ return (jsxRuntime.jsx(IconButton, { disabled: disabled, icon: "call-accept", variant: "success", onClick: handleClick }));
962
+ };
963
+
964
+ const Notification = (props) => {
965
+ const { isVisible, message, children, visibilityTimeout, resetIsVisible, placement = 'top', iconClassName = 'str-video__notification__icon', } = props;
966
+ const { refs, x, y, strategy } = useFloatingUIPreset({
967
+ placement,
968
+ strategy: 'absolute',
969
+ });
970
+ react.useEffect(() => {
971
+ if (!isVisible || !visibilityTimeout || !resetIsVisible)
972
+ return;
973
+ const timeout = setTimeout(() => {
974
+ resetIsVisible();
975
+ }, visibilityTimeout);
976
+ return () => clearTimeout(timeout);
977
+ }, [isVisible, resetIsVisible, visibilityTimeout]);
978
+ return (jsxRuntime.jsxs("div", { ref: refs.setReference, children: [isVisible && (jsxRuntime.jsxs("div", { className: "str-video__notification", ref: refs.setFloating, style: {
979
+ position: strategy,
980
+ top: y ?? 0,
981
+ left: x ?? 0,
982
+ overflowY: 'auto',
983
+ }, children: [iconClassName && jsxRuntime.jsx("i", { className: iconClassName }), jsxRuntime.jsx("span", { className: "str-video__notification__message", children: message })] })), children] }));
984
+ };
985
+
986
+ const PermissionNotification = (props) => {
987
+ const { permission, isAwaitingApproval, messageApproved, messageAwaitingApproval, messageRevoked, visibilityTimeout = 3500, children, } = props;
988
+ const hasPermission = videoReactBindings.useHasPermissions(permission);
989
+ const prevHasPermission = react.useRef(hasPermission);
990
+ const [showNotification, setShowNotification] = react.useState();
991
+ react.useEffect(() => {
992
+ if (hasPermission && !prevHasPermission.current) {
993
+ setShowNotification('granted');
994
+ prevHasPermission.current = true;
995
+ }
996
+ else if (!hasPermission && prevHasPermission.current) {
997
+ setShowNotification('revoked');
998
+ prevHasPermission.current = false;
999
+ }
1000
+ }, [hasPermission]);
1001
+ const resetIsVisible = react.useCallback(() => setShowNotification(undefined), []);
1002
+ if (isAwaitingApproval) {
1003
+ return (jsxRuntime.jsx(Notification, { isVisible: isAwaitingApproval && !hasPermission, message: messageAwaitingApproval, children: children }));
1004
+ }
1005
+ return (jsxRuntime.jsx(Notification, { isVisible: !!showNotification, visibilityTimeout: visibilityTimeout, resetIsVisible: resetIsVisible, message: showNotification === 'granted' ? messageApproved : messageRevoked, children: children }));
1006
+ };
1007
+
1008
+ const SpeakingWhileMutedNotification = ({ children, text, }) => {
1009
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
1010
+ const localParticipant = useLocalParticipant();
1011
+ const { getAudioStream } = useMediaDevices();
1012
+ const { t } = videoReactBindings.useI18n();
1013
+ const message = text ?? t('You are muted. Unmute to speak.');
1014
+ const isAudioMute = !localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1015
+ const audioDeviceId = localParticipant?.audioDeviceId;
1016
+ const [isSpeakingWhileMuted, setIsSpeakingWhileMuted] = react.useState(false);
1017
+ react.useEffect(() => {
1018
+ // do nothing when not muted
1019
+ if (!isAudioMute)
1020
+ return;
1021
+ const disposeSoundDetector = getAudioStream({
1022
+ deviceId: audioDeviceId,
1023
+ }).then((audioStream) => videoClient.createSoundDetector(audioStream, ({ isSoundDetected }) => {
1024
+ setIsSpeakingWhileMuted((isNotified) => isNotified ? isNotified : isSoundDetected);
1025
+ }));
1026
+ disposeSoundDetector.catch((err) => {
1027
+ console.error('Error while creating sound detector', err);
1028
+ });
1029
+ return () => {
1030
+ disposeSoundDetector
1031
+ .then((dispose) => dispose())
1032
+ .catch((err) => {
1033
+ console.error('Error while disposing sound detector', err);
1034
+ });
1035
+ setIsSpeakingWhileMuted(false);
1036
+ };
1037
+ }, [audioDeviceId, getAudioStream, isAudioMute]);
1038
+ react.useEffect(() => {
1039
+ if (!isSpeakingWhileMuted)
1040
+ return;
1041
+ const timeout = setTimeout(() => {
1042
+ setIsSpeakingWhileMuted(false);
1043
+ }, 3500);
1044
+ return () => {
1045
+ clearTimeout(timeout);
1046
+ setIsSpeakingWhileMuted(false);
1047
+ };
1048
+ }, [isSpeakingWhileMuted]);
1049
+ return (jsxRuntime.jsx(Notification, { message: message, isVisible: isSpeakingWhileMuted, children: children }));
1050
+ };
1051
+
1052
+ const CallControls = ({ onLeave }) => (jsxRuntime.jsxs("div", { className: "str-video__call-controls", children: [jsxRuntime.jsx(RecordCallButton, {}), jsxRuntime.jsx(CallStatsButton, {}), jsxRuntime.jsx(ScreenShareButton, {}), jsxRuntime.jsx(SpeakingWhileMutedNotification, { children: jsxRuntime.jsx(ToggleAudioPublishingButton, {}) }), jsxRuntime.jsx(ToggleVideoPublishingButton, {}), jsxRuntime.jsx(CancelCallButton, { onLeave: onLeave })] }));
1053
+
1054
+ const CallStatsLatencyChart = (props) => {
1055
+ const { values } = props;
1056
+ let max = 0;
1057
+ const data = values.map((point) => {
1058
+ const { y } = point;
1059
+ max = Math.max(max, y);
1060
+ return point;
1061
+ });
1062
+ return (jsxRuntime.jsx("div", { className: "str-video__call-stats-line-chart-container", children: jsxRuntime.jsx(line.ResponsiveLine, { colors: { scheme: 'blues' }, data: [
1063
+ {
1064
+ id: 'Latency',
1065
+ data: data,
1066
+ },
1067
+ ], animate: false, margin: { top: 10, right: 5, bottom: 5, left: 30 }, enablePoints: true, enableGridX: false, enableGridY: true, enableSlices: "x", isInteractive: true, useMesh: false, xScale: { type: 'point' }, yScale: {
1068
+ type: 'linear',
1069
+ min: 0,
1070
+ max: max < 220 ? 220 : max + 30,
1071
+ nice: true,
1072
+ }, theme: {
1073
+ axis: {
1074
+ ticks: {
1075
+ text: {
1076
+ fill: '#FCFCFD',
1077
+ },
1078
+ line: {
1079
+ stroke: '#FCFCFD',
1080
+ },
1081
+ },
1082
+ },
1083
+ grid: {
1084
+ line: {
1085
+ strokeWidth: 0.1,
1086
+ },
1087
+ },
1088
+ } }) }));
1089
+ };
1090
+
1091
+ const CallStats = () => {
1092
+ const [latencyBuffer, setLatencyBuffer] = react.useState(() => {
1093
+ const now = Date.now();
1094
+ return Array.from({ length: 20 }, (_, i) => ({ x: now + i, y: 0 }));
1095
+ });
1096
+ const [publishBitrate, setPublishBitrate] = react.useState('-');
1097
+ const [subscribeBitrate, setSubscribeBitrate] = react.useState('-');
1098
+ const previousStats = react.useRef();
1099
+ const { useCallStatsReport } = videoReactBindings.useCallStateHooks();
1100
+ const callStatsReport = useCallStatsReport();
1101
+ react.useEffect(() => {
1102
+ if (!callStatsReport)
1103
+ return;
1104
+ if (!previousStats.current) {
1105
+ previousStats.current = callStatsReport;
1106
+ return;
1107
+ }
1108
+ const previousCallStatsReport = previousStats.current;
1109
+ setPublishBitrate(() => {
1110
+ return calculatePublishBitrate(previousCallStatsReport, callStatsReport);
1111
+ });
1112
+ setSubscribeBitrate(() => {
1113
+ return calculateSubscribeBitrate(previousCallStatsReport, callStatsReport);
1114
+ });
1115
+ setLatencyBuffer((latencyBuf) => {
1116
+ const newLatencyBuffer = latencyBuf.slice(-19);
1117
+ newLatencyBuffer.push({
1118
+ x: callStatsReport.timestamp,
1119
+ y: callStatsReport.publisherStats.averageRoundTripTimeInMs,
1120
+ });
1121
+ return newLatencyBuffer;
1122
+ });
1123
+ previousStats.current = callStatsReport;
1124
+ }, [callStatsReport]);
1125
+ return (jsxRuntime.jsx("div", { className: "str-video__call-stats", children: callStatsReport && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { children: "Call Latency" }), jsxRuntime.jsx(CallStatsLatencyChart, { values: latencyBuffer }), jsxRuntime.jsx("h3", { children: "Call performance" }), jsxRuntime.jsxs("div", { className: "str-video__call-stats__card-container", children: [jsxRuntime.jsx(StatCard, { label: "Region", value: callStatsReport.datacenter }), jsxRuntime.jsx(StatCard, { label: "Latency", value: `${callStatsReport.publisherStats.averageRoundTripTimeInMs} ms.` }), jsxRuntime.jsx(StatCard, { label: "Receive jitter", value: `${callStatsReport.subscriberStats.averageJitterInMs} ms.` }), jsxRuntime.jsx(StatCard, { label: "Publish jitter", value: `${callStatsReport.publisherStats.averageJitterInMs} ms.` }), jsxRuntime.jsx(StatCard, { label: "Publish resolution", value: toFrameSize(callStatsReport.publisherStats) }), jsxRuntime.jsx(StatCard, { label: "Publish quality drop reason", value: callStatsReport.publisherStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: "Receiving resolution", value: toFrameSize(callStatsReport.subscriberStats) }), jsxRuntime.jsx(StatCard, { label: "Receive quality drop reason", value: callStatsReport.subscriberStats.qualityLimitationReasons }), jsxRuntime.jsx(StatCard, { label: "Publish bitrate", value: publishBitrate }), jsxRuntime.jsx(StatCard, { label: "Receiving bitrate", value: subscribeBitrate })] })] })) }));
1126
+ };
1127
+ const StatCard = (props) => {
1128
+ const { label, value } = props;
1129
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-stats__card", children: [jsxRuntime.jsx("div", { className: "str-video__call-stats__card_label", children: label }), jsxRuntime.jsx("div", { className: "str-video__call-stats__card_value", children: value })] }));
1130
+ };
1131
+ const toFrameSize = (stats) => {
1132
+ const { highestFrameWidth: w, highestFrameHeight: h, highestFramesPerSecond: fps, } = stats;
1133
+ let size = `-`;
1134
+ if (w && h) {
1135
+ size = `${w}x${h}`;
1136
+ if (fps) {
1137
+ size += `@${fps}fps.`;
1138
+ }
1139
+ }
1140
+ return size;
1141
+ };
1142
+ const calculatePublishBitrate = (previousCallStatsReport, callStatsReport) => {
1143
+ const { publisherStats: { totalBytesSent: previousTotalBytesSent, timestamp: previousTimestamp, }, } = previousCallStatsReport;
1144
+ const { publisherStats: { totalBytesSent, timestamp }, } = callStatsReport;
1145
+ const bytesSent = totalBytesSent - previousTotalBytesSent;
1146
+ const timeElapsed = timestamp - previousTimestamp;
1147
+ return `${((bytesSent * 8) / timeElapsed).toFixed(2)} kbps`;
1148
+ };
1149
+ const calculateSubscribeBitrate = (previousCallStatsReport, callStatsReport) => {
1150
+ const { subscriberStats: { totalBytesReceived: previousTotalBytesReceived, timestamp: previousTimestamp, }, } = previousCallStatsReport;
1151
+ const { subscriberStats: { totalBytesReceived, timestamp }, } = callStatsReport;
1152
+ const bytesReceived = totalBytesReceived - previousTotalBytesReceived;
1153
+ const timeElapsed = timestamp - previousTimestamp;
1154
+ return `${((bytesReceived * 8) / timeElapsed).toFixed(2)} kbps`;
1155
+ };
1156
+
1157
+ const CallStatsButton = () => (jsxRuntime.jsx(MenuToggle, { placement: "top-end", ToggleButton: ToggleMenuButton$1, children: jsxRuntime.jsx(CallStats, {}) }));
1158
+ const ToggleMenuButton$1 = react.forwardRef(({ menuShown }, ref) => (jsxRuntime.jsx(CompositeButton, { ref: ref, active: menuShown, caption: 'Stats', children: jsxRuntime.jsx(IconButton, { icon: "stats", title: "Statistics" }) })));
1159
+
1160
+ const CancelCallButton = ({ disabled, onClick, onLeave, }) => {
1161
+ const call = videoReactBindings.useCall();
1162
+ const handleClick = react.useCallback(async (e) => {
1163
+ if (onClick) {
1164
+ onClick(e);
1165
+ }
1166
+ else if (call) {
1167
+ await call.leave();
1168
+ onLeave?.();
1169
+ }
1170
+ }, [onClick, onLeave, call]);
1171
+ return (jsxRuntime.jsx(IconButton, { disabled: disabled, icon: "call-end", variant: "danger", onClick: handleClick }));
1172
+ };
1173
+
1174
+ const defaultEmojiReactionMap = {
1175
+ ':like:': '👍',
1176
+ ':raise-hand:': '✋',
1177
+ ':fireworks:': '🎉',
1178
+ ':dislike:': '👎',
1179
+ ':heart:': '❤️',
1180
+ ':smile:': '😀',
1181
+ };
1182
+ const Reaction = ({ participant: { reaction, sessionId }, hideAfterTimeoutInMs = 5500, emojiReactionMap = defaultEmojiReactionMap, }) => {
1183
+ const call = videoReactBindings.useCall();
1184
+ react.useEffect(() => {
1185
+ if (!call || !reaction)
1186
+ return;
1187
+ const timeoutId = setTimeout(() => {
1188
+ call.resetReaction(sessionId);
1189
+ }, hideAfterTimeoutInMs);
1190
+ return () => {
1191
+ clearTimeout(timeoutId);
1192
+ };
1193
+ }, [call, hideAfterTimeoutInMs, reaction, sessionId]);
1194
+ if (!reaction)
1195
+ return null;
1196
+ const { emoji_code: emojiCode } = reaction;
1197
+ return (jsxRuntime.jsx("div", { className: "str-video__reaction", children: jsxRuntime.jsx("span", { className: "str-video__reaction__emoji", children: emojiCode && emojiReactionMap[emojiCode] }) }));
1198
+ };
1199
+
1200
+ const defaultReactions = [
1201
+ {
1202
+ type: 'reaction',
1203
+ emoji_code: ':like:',
1204
+ },
1205
+ {
1206
+ // TODO OL: use `prompt` type?
1207
+ type: 'raised-hand',
1208
+ emoji_code: ':raise-hand:',
1209
+ },
1210
+ {
1211
+ type: 'reaction',
1212
+ emoji_code: ':fireworks:',
1213
+ },
1214
+ {
1215
+ type: 'reaction',
1216
+ emoji_code: ':dislike:',
1217
+ },
1218
+ {
1219
+ type: 'reaction',
1220
+ emoji_code: ':heart:',
1221
+ },
1222
+ {
1223
+ type: 'reaction',
1224
+ emoji_code: ':smile:',
1225
+ },
1226
+ ];
1227
+ const ReactionsButton = ({ reactions = defaultReactions, }) => {
1228
+ const { t } = videoReactBindings.useI18n();
1229
+ return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.CREATE_REACTION], children: jsxRuntime.jsx(CompositeButton, { active: false, caption: t('Reactions'), menuPlacement: "top-start", Menu: jsxRuntime.jsx(DefaultReactionsMenu, { reactions: reactions }), children: jsxRuntime.jsx(IconButton, { icon: "reactions", title: t('Reactions'), onClick: () => {
1230
+ console.log('Reactions');
1231
+ } }) }) }));
1232
+ };
1233
+ const DefaultReactionsMenu = ({ reactions, }) => {
1234
+ const call = videoReactBindings.useCall();
1235
+ return (jsxRuntime.jsx("div", { className: "str-video__reactions-menu", children: reactions.map((reaction) => (jsxRuntime.jsx("button", { type: "button", className: "str-video__reactions-menu__button", onClick: () => {
1236
+ call?.sendReaction(reaction);
1237
+ }, children: reaction.emoji_code && defaultEmojiReactionMap[reaction.emoji_code] }, reaction.emoji_code))) }));
1238
+ };
1239
+
1240
+ const LoadingIndicator = ({ className, type = 'spinner', text, tooltip, }) => {
1241
+ 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 })] }));
1242
+ };
1243
+
1244
+ const RecordCallButton = ({ caption = 'Record', }) => {
1245
+ const call = videoReactBindings.useCall();
1246
+ const { t } = videoReactBindings.useI18n();
1247
+ const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = useToggleCallRecording();
1248
+ return (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [
1249
+ videoClient.OwnCapability.START_RECORD_CALL,
1250
+ videoClient.OwnCapability.STOP_RECORD_CALL,
1251
+ ], children: jsxRuntime.jsx(CompositeButton, { active: isCallRecordingInProgress, caption: caption, children: isAwaitingResponse ? (jsxRuntime.jsx(LoadingIndicator, { tooltip: isCallRecordingInProgress
1252
+ ? t('Waiting for recording to stop...')
1253
+ : t('Waiting for recording to start...') })) : (jsxRuntime.jsx(IconButton
1254
+ // FIXME OL: sort out this ambiguity
1255
+ , {
1256
+ // FIXME OL: sort out this ambiguity
1257
+ enabled: !!call, disabled: !call, icon: isCallRecordingInProgress ? 'recording-on' : 'recording-off', title: t('Record call'), onClick: toggleCallRecording })) }) }));
1258
+ };
1259
+
1260
+ const ScreenShareButton = ({ caption = 'Screen Share', }) => {
1261
+ const call = videoReactBindings.useCall();
1262
+ const { useHasOngoingScreenShare } = videoReactBindings.useCallStateHooks();
1263
+ const isSomeoneScreenSharing = useHasOngoingScreenShare();
1264
+ const { t } = videoReactBindings.useI18n();
1265
+ const { toggleScreenShare, isAwaitingPermission, isScreenSharing } = useToggleScreenShare();
1266
+ 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(CompositeButton, { active: isSomeoneScreenSharing, caption: caption, children: jsxRuntime.jsx(IconButton, { icon: isScreenSharing ? 'screen-share-on' : 'screen-share-off', title: t('Share screen'), disabled: (!isScreenSharing && isSomeoneScreenSharing) || !call, onClick: toggleScreenShare }) }) }) }));
1267
+ };
1268
+
1269
+ const DeviceSelectorOption = ({ disabled, id, label, onChange, name, selected, defaultChecked, value, }) => {
1270
+ return (jsxRuntime.jsxs("label", { className: clsx('str-video__device-settings__option', {
1271
+ 'str-video__device-settings__option--selected': selected,
1272
+ 'str-video__device-settings__option--disabled': disabled,
1273
+ }), htmlFor: id, children: [jsxRuntime.jsx("input", { type: "radio", name: name, onChange: onChange, value: value, id: id, checked: selected, defaultChecked: defaultChecked, disabled: disabled }), label] }));
1274
+ };
1275
+ const DeviceSelector = (props) => {
1276
+ const { devices = [], selectedDeviceId: selectedDeviceFromProps, title, onChange, } = props;
1277
+ const inputGroupName = title.replace(' ', '-').toLowerCase();
1278
+ // sometimes the browser (Chrome) will report the system-default device
1279
+ // with an id of 'default'. In case when it doesn't, we'll select the first
1280
+ // available device.
1281
+ let selectedDeviceId = selectedDeviceFromProps;
1282
+ if (devices.length > 0 &&
1283
+ !devices.find((d) => d.deviceId === selectedDeviceId)) {
1284
+ selectedDeviceId = devices[0].deviceId;
1285
+ }
1286
+ 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 }), !devices.length ? (jsxRuntime.jsx(DeviceSelectorOption, { id: `${inputGroupName}--default`, label: "Default", name: inputGroupName, defaultChecked: true, value: "default" })) : (devices.map((device) => {
1287
+ return (jsxRuntime.jsx(DeviceSelectorOption, { id: `${inputGroupName}--${device.deviceId}`, value: device.deviceId, label: device.label, onChange: (e) => {
1288
+ onChange?.(e.target.value);
1289
+ }, name: inputGroupName, selected: device.deviceId === selectedDeviceId || devices.length === 1 }, device.deviceId));
1290
+ }))] }));
1291
+ };
1292
+
1293
+ const DeviceSelectorAudioInput = ({ title = 'Select a Mic', }) => {
1294
+ const { selectedAudioInputDeviceId, switchDevice } = useMediaDevices();
1295
+ const audioInputDevices = useAudioInputDevices();
1296
+ return (jsxRuntime.jsx(DeviceSelector, { devices: audioInputDevices, selectedDeviceId: selectedAudioInputDeviceId, onChange: (deviceId) => {
1297
+ switchDevice('audioinput', deviceId);
1298
+ }, title: title }));
1299
+ };
1300
+ const DeviceSelectorAudioOutput = ({ title = 'Select Speakers', }) => {
1301
+ const { isAudioOutputChangeSupported, selectedAudioOutputDeviceId, switchDevice, } = useMediaDevices();
1302
+ const audioOutputDevices = useAudioOutputDevices();
1303
+ if (!isAudioOutputChangeSupported)
1304
+ return null;
1305
+ return (jsxRuntime.jsx(DeviceSelector, { devices: audioOutputDevices, selectedDeviceId: selectedAudioOutputDeviceId, onChange: (deviceId) => {
1306
+ switchDevice('audiooutput', deviceId);
1307
+ }, title: title }));
1308
+ };
1309
+
1310
+ const DeviceSelectorVideo = ({ title }) => {
1311
+ const { selectedVideoDeviceId, switchDevice } = useMediaDevices();
1312
+ const videoDevices = useVideoDevices();
1313
+ return (jsxRuntime.jsx(DeviceSelector, { devices: videoDevices, selectedDeviceId: selectedVideoDeviceId, onChange: (deviceId) => {
1314
+ switchDevice('videoinput', deviceId);
1315
+ }, title: title || 'Select a Camera' }));
1316
+ };
1317
+
1318
+ const DeviceSettings = () => {
1319
+ return (jsxRuntime.jsx(MenuToggle, { placement: "bottom-end", ToggleButton: ToggleMenuButton, children: jsxRuntime.jsx(Menu, {}) }));
1320
+ };
1321
+ const Menu = () => (jsxRuntime.jsxs("div", { className: "str-video__device-settings", children: [jsxRuntime.jsx(DeviceSelectorVideo, {}), jsxRuntime.jsx(DeviceSelectorAudioInput, {}), jsxRuntime.jsx(DeviceSelectorAudioOutput, {})] }));
1322
+ const ToggleMenuButton = react.forwardRef(({ menuShown }, ref) => (jsxRuntime.jsx(IconButton, { className: clsx('str-video__device-settings__button', {
1323
+ 'str-video__device-settings__button--active': menuShown,
1324
+ }), title: "Toggle device menu", icon: "device-settings", ref: ref })));
1325
+
1326
+ const ToggleAudioPreviewButton = (props) => {
1327
+ const { initialAudioEnabled, toggleInitialAudioMuteState } = useMediaDevices();
1328
+ const { t } = videoReactBindings.useI18n();
1329
+ const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
1330
+ return (jsxRuntime.jsx(CompositeButton, { Menu: Menu, active: !initialAudioEnabled, caption: caption || t('Mic'), children: jsxRuntime.jsx(IconButton, { icon: initialAudioEnabled ? 'mic' : 'mic-off', onClick: toggleInitialAudioMuteState }) }));
1331
+ };
1332
+ const ToggleAudioPublishingButton = (props) => {
1333
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
1334
+ const localParticipant = useLocalParticipant();
1335
+ const { t } = videoReactBindings.useI18n();
1336
+ const { caption = t('Mic'), Menu = DeviceSelectorAudioInput } = props;
1337
+ const isAudioMute = !localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1338
+ const { toggleAudioMuteState: handleClick, isAwaitingPermission } = useToggleAudioMuteState();
1339
+ 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(CompositeButton, { Menu: Menu, active: isAudioMute, caption: caption, children: jsxRuntime.jsx(IconButton, { icon: isAudioMute ? 'mic-off' : 'mic', onClick: handleClick }) }) }) }));
1340
+ };
1341
+
1342
+ const ToggleAudioOutputButton = (props) => {
1343
+ const { t } = videoReactBindings.useI18n();
1344
+ const { caption = t('Speakers'), Menu = DeviceSelectorAudioOutput } = props;
1345
+ return (jsxRuntime.jsx(CompositeButton, { Menu: Menu, active: true, caption: caption, children: jsxRuntime.jsx(IconButton, { icon: "speaker" }) }));
1346
+ };
1347
+
1348
+ const ToggleVideoPreviewButton = (props) => {
1349
+ const { toggleInitialVideoMuteState, initialVideoState } = useMediaDevices();
1350
+ const { t } = videoReactBindings.useI18n();
1351
+ const { caption = t('Video'), Menu = DeviceSelectorVideo } = props;
1352
+ return (jsxRuntime.jsx(CompositeButton, { Menu: Menu, active: !initialVideoState.enabled, caption: caption, children: jsxRuntime.jsx(IconButton, { icon: initialVideoState.enabled ? 'camera' : 'camera-off', onClick: toggleInitialVideoMuteState }) }));
1353
+ };
1354
+ const ToggleVideoPublishingButton = (props) => {
1355
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
1356
+ const localParticipant = useLocalParticipant();
1357
+ const { t } = videoReactBindings.useI18n();
1358
+ const { caption = t('Video'), Menu = DeviceSelectorVideo } = props;
1359
+ const isVideoMute = !localParticipant?.publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
1360
+ const { toggleVideoMuteState: handleClick, isAwaitingPermission } = useToggleVideoMuteState();
1361
+ 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(CompositeButton, { Menu: Menu, active: isVideoMute, caption: caption, children: jsxRuntime.jsx(IconButton, { icon: isVideoMute ? 'camera-off' : 'camera', onClick: handleClick }) }) }) }));
1362
+ };
1363
+
1364
+ const BlockedUserListing = ({ data }) => {
1365
+ if (!data.length)
1366
+ return null;
1367
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx("div", { className: "str-video__participant-listing", children: data.map((userId) => (jsxRuntime.jsx(BlockedUserListingItem, { userId: userId }, userId))) }) }));
1368
+ };
1369
+ const BlockedUserListingItem = ({ userId }) => {
1370
+ const call = videoReactBindings.useCall();
1371
+ const unblockUserClickHandler = () => {
1372
+ if (userId)
1373
+ call?.unblockUser(userId);
1374
+ };
1375
+ 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" }) })] }));
1376
+ };
1377
+
1378
+ const CallParticipantListHeader = ({ onClose, }) => {
1379
+ const { useParticipants, useAnonymousParticipantCount } = videoReactBindings.useCallStateHooks();
1380
+ const participants = useParticipants();
1381
+ const anonymousParticipantCount = useAnonymousParticipantCount();
1382
+ const { t } = videoReactBindings.useI18n();
1383
+ 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("button", { onClick: onClose, className: "str-video__participant-list-header__close-button", children: jsxRuntime.jsx("span", { className: "str-video__participant-list-header__close-button--icon" }) })] }));
1384
+ };
1385
+
1386
+ const CallParticipantListingItem = ({ participant, DisplayName = DefaultDisplayName, }) => {
1387
+ const isAudioOn = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1388
+ const isVideoOn = participant.publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
1389
+ const isPinned = !!participant.pin;
1390
+ const { t } = videoReactBindings.useI18n();
1391
+ return (jsxRuntime.jsxs("div", { className: "str-video__participant-listing-item", children: [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'}`) }), isPinned && (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(ParticipantActionsContextMenu, { participant: participant }) })] })] }));
1392
+ };
1393
+ const MediaIndicator = (props) => (jsxRuntime.jsx(WithTooltip, { ...props }));
1394
+ // todo: implement display device flag
1395
+ const DefaultDisplayName = ({ participant }) => {
1396
+ const connectedUser = videoReactBindings.useConnectedUser();
1397
+ const { t } = videoReactBindings.useI18n();
1398
+ const meFlag = participant.userId === connectedUser?.id ? t('Me') : '';
1399
+ const nameOrId = participant.name || participant.userId || t('Unknown');
1400
+ let displayName;
1401
+ if (!participant.name) {
1402
+ displayName = meFlag || nameOrId || t('Unknown');
1403
+ }
1404
+ else if (meFlag) {
1405
+ displayName = `${nameOrId} (${meFlag})`;
1406
+ }
1407
+ else {
1408
+ displayName = nameOrId;
1409
+ }
1410
+ return (jsxRuntime.jsx(WithTooltip, { className: "str-video__participant-listing-item__display-name", title: displayName, children: displayName }));
1411
+ };
1412
+ const ToggleButton$2 = react.forwardRef((props, ref) => {
1413
+ return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
1414
+ });
1415
+ const ParticipantActionsContextMenu = ({ participant, participantViewElement, videoElement, }) => {
1416
+ const [fullscreenModeOn, setFullscreenModeOn] = react.useState(!!document.fullscreenElement);
1417
+ const [pictureInPictureElement, setPictureInPictureElement] = react.useState(document.pictureInPictureElement);
1418
+ const call = videoReactBindings.useCall();
1419
+ const { t } = videoReactBindings.useI18n();
1420
+ const { pin, publishedTracks, sessionId, userId } = participant;
1421
+ const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
1422
+ const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
1423
+ const hasScreenShare = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
1424
+ const hasScreenShareAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
1425
+ const blockUser = () => call?.blockUser(userId);
1426
+ const muteAudio = () => call?.muteUser(userId, 'audio');
1427
+ const muteVideo = () => call?.muteUser(userId, 'video');
1428
+ const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
1429
+ const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
1430
+ const grantPermission = (permission) => () => {
1431
+ call?.updateUserPermissions({
1432
+ user_id: userId,
1433
+ grant_permissions: [permission],
1434
+ });
1435
+ };
1436
+ const revokePermission = (permission) => () => {
1437
+ call?.updateUserPermissions({
1438
+ user_id: userId,
1439
+ revoke_permissions: [permission],
1440
+ });
1441
+ };
1442
+ const toggleParticipantPinnedAt = () => {
1443
+ if (pin) {
1444
+ call?.unpin(sessionId);
1445
+ }
1446
+ else {
1447
+ call?.pin(sessionId);
1448
+ }
1449
+ };
1450
+ const pinForEveryone = () => {
1451
+ call
1452
+ ?.pinForEveryone({
1453
+ user_id: userId,
1454
+ session_id: sessionId,
1455
+ })
1456
+ .catch((err) => {
1457
+ console.error(`Failed to pin participant ${userId}`, err);
1458
+ });
1459
+ };
1460
+ const unpinForEveryone = () => {
1461
+ call
1462
+ ?.unpinForEveryone({
1463
+ user_id: userId,
1464
+ session_id: sessionId,
1465
+ })
1466
+ .catch((err) => {
1467
+ console.error(`Failed to unpin participant ${userId}`, err);
1468
+ });
1469
+ };
1470
+ const toggleFullscreenMode = () => {
1471
+ if (!fullscreenModeOn) {
1472
+ return participantViewElement
1473
+ ?.requestFullscreen()
1474
+ .then(() => setFullscreenModeOn(true))
1475
+ .catch(console.error);
1476
+ }
1477
+ document
1478
+ .exitFullscreen()
1479
+ .catch(console.error)
1480
+ .finally(() => setFullscreenModeOn(false));
1481
+ };
1482
+ react.useEffect(() => {
1483
+ if (!videoElement)
1484
+ return;
1485
+ const handlePictureInPicture = () => {
1486
+ setPictureInPictureElement(document.pictureInPictureElement);
1487
+ };
1488
+ videoElement.addEventListener('enterpictureinpicture', handlePictureInPicture);
1489
+ videoElement.addEventListener('leavepictureinpicture', handlePictureInPicture);
1490
+ return () => {
1491
+ videoElement.removeEventListener('enterpictureinpicture', handlePictureInPicture);
1492
+ videoElement.removeEventListener('leavepictureinpicture', handlePictureInPicture);
1493
+ };
1494
+ }, [videoElement]);
1495
+ const togglePictureInPicture = () => {
1496
+ if (videoElement && pictureInPictureElement !== videoElement) {
1497
+ return videoElement
1498
+ .requestPictureInPicture()
1499
+ .catch(console.error);
1500
+ }
1501
+ document.exitPictureInPicture().catch(console.error);
1502
+ };
1503
+ return (jsxRuntime.jsxs(GenericMenu, { children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPinnedAt, 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.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { disabled: !hasVideo, onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { disabled: !hasScreenShare, onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { disabled: !hasAudio, onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { disabled: !hasScreenShareAudio, onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] })] }), participantViewElement && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
1504
+ direction: fullscreenModeOn ? t('Leave') : t('Enter'),
1505
+ }) })), videoElement && document.pictureInPictureEnabled && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
1506
+ direction: pictureInPictureElement === videoElement
1507
+ ? t('Leave')
1508
+ : t('Enter'),
1509
+ }) })), 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') })] })] }));
1510
+ };
1511
+
1512
+ const CallParticipantListing = ({ data, }) => (jsxRuntime.jsx("div", { className: "str-video__participant-listing", children: data.map((participant) => (jsxRuntime.jsx(CallParticipantListingItem, { participant: participant }, participant.sessionId))) }));
1513
+
1514
+ const EmptyParticipantSearchList = () => {
1515
+ const { t } = videoReactBindings.useI18n();
1516
+ return (jsxRuntime.jsx("div", { className: "str-video__participant-list--empty", children: t('No participants found') }));
1517
+ };
1518
+
1519
+ const SearchInput = ({ exitSearch, isActive, ...rest }) => {
1520
+ const [inputElement, setInputElement] = react.useState(null);
1521
+ react.useEffect(() => {
1522
+ if (!inputElement)
1523
+ return;
1524
+ const handleKeyDown = (e) => {
1525
+ if (e.key.toLowerCase() === 'escape')
1526
+ exitSearch();
1527
+ };
1528
+ inputElement.addEventListener('keydown', handleKeyDown);
1529
+ return () => {
1530
+ inputElement.removeEventListener('keydown', handleKeyDown);
1531
+ };
1532
+ }, [exitSearch, inputElement]);
1533
+ return (jsxRuntime.jsxs("div", { className: clsx('str-video__search-input__container', {
1534
+ 'str-video__search-input__container--active': isActive,
1535
+ }), 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" }))] }));
1536
+ };
1537
+
1538
+ const SearchResults = ({ EmptySearchResultComponent, LoadingIndicator: LoadingIndicator$1 = LoadingIndicator, searchQueryInProgress, searchResults, SearchResultList, }) => {
1539
+ if (searchQueryInProgress) {
1540
+ return (jsxRuntime.jsx("div", { className: "str-video__search-results--loading", children: jsxRuntime.jsx(LoadingIndicator$1, {}) }));
1541
+ }
1542
+ if (!searchResults.length) {
1543
+ return jsxRuntime.jsx(EmptySearchResultComponent, {});
1544
+ }
1545
+ return jsxRuntime.jsx(SearchResultList, { data: searchResults });
1546
+ };
1547
+
1548
+ const useSearch = ({ debounceInterval, searchFn, searchQuery = '', }) => {
1549
+ const [searchResults, setSearchResults] = react.useState([]);
1550
+ const [searchQueryInProgress, setSearchQueryInProgress] = react.useState(false);
1551
+ react.useEffect(() => {
1552
+ if (!searchQuery.length)
1553
+ return setSearchResults([]);
1554
+ setSearchQueryInProgress(true);
1555
+ const timeout = setTimeout(async () => {
1556
+ try {
1557
+ const results = await searchFn(searchQuery);
1558
+ setSearchResults(results);
1559
+ }
1560
+ catch (error) {
1561
+ console.error(error);
1562
+ }
1563
+ finally {
1564
+ setSearchQueryInProgress(false);
1565
+ }
1566
+ }, debounceInterval);
1567
+ return () => {
1568
+ clearTimeout(timeout);
1569
+ };
1570
+ }, [debounceInterval, searchFn, searchQuery]);
1571
+ return {
1572
+ searchQueryInProgress,
1573
+ searchResults,
1574
+ };
1575
+ };
1576
+
1577
+ const UserListTypes = {
1578
+ active: 'Active users',
1579
+ blocked: 'Blocked users',
1580
+ };
1581
+ const DEFAULT_DEBOUNCE_SEARCH_INTERVAL = 200;
1582
+ const CallParticipantsList = ({ onClose, activeUsersSearchFn, blockedUsersSearchFn, debounceSearchInterval, }) => {
1583
+ const [searchQuery, setSearchQuery] = react.useState('');
1584
+ const [userListType, setUserListType] = react.useState('active');
1585
+ const exitSearch = react.useCallback(() => setSearchQuery(''), []);
1586
+ 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 }))] }), jsxRuntime.jsx("div", { className: "str-video__participant-list__footer", children: jsxRuntime.jsx(CopyToClipboardButtonWithPopup, { Button: InviteLinkButton, copyValue: typeof window !== 'undefined' ? window.location.href : '' }) })] }));
1587
+ };
1588
+ const CallParticipantListContentHeader = ({ userListType, setUserListType, }) => {
1589
+ const call = videoReactBindings.useCall();
1590
+ const muteAll = () => {
1591
+ call?.muteAllUsers('audio');
1592
+ };
1593
+ return (jsxRuntime.jsxs("div", { className: "str-video__participant-list__content-header", children: [jsxRuntime.jsxs("div", { className: "str-video__participant-list__content-header-title", children: [jsxRuntime.jsx("span", { children: UserListTypes[userListType] }), userListType === 'active' && (jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], hasPermissionsOnly: true, children: jsxRuntime.jsx(TextButton, { onClick: muteAll, children: "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))) }) })] }));
1594
+ };
1595
+ const ActiveUsersSearchResults = ({ searchQuery, activeUsersSearchFn: activeUsersSearchFnFromProps, debounceSearchInterval = DEFAULT_DEBOUNCE_SEARCH_INTERVAL, }) => {
1596
+ const { useParticipants } = videoReactBindings.useCallStateHooks();
1597
+ const participants = useParticipants({ sortBy: videoClient.name });
1598
+ const activeUsersSearchFn = react.useCallback((queryString) => {
1599
+ const queryRegExp = new RegExp(queryString, 'i');
1600
+ return Promise.resolve(participants.filter((participant) => {
1601
+ return participant.name.match(queryRegExp);
1602
+ }));
1603
+ }, [participants]);
1604
+ const { searchQueryInProgress, searchResults } = useSearch({
1605
+ searchFn: activeUsersSearchFnFromProps ?? activeUsersSearchFn,
1606
+ debounceInterval: debounceSearchInterval,
1607
+ searchQuery,
1608
+ });
1609
+ return (jsxRuntime.jsx(SearchResults, { EmptySearchResultComponent: EmptyParticipantSearchList, LoadingIndicator: LoadingIndicator, searchQueryInProgress: searchQueryInProgress, searchResults: searchQuery ? searchResults : participants, SearchResultList: CallParticipantListing }));
1610
+ };
1611
+ const BlockedUsersSearchResults = ({ blockedUsersSearchFn: blockedUsersSearchFnFromProps, debounceSearchInterval = DEFAULT_DEBOUNCE_SEARCH_INTERVAL, searchQuery, }) => {
1612
+ const { useCallBlockedUserIds } = videoReactBindings.useCallStateHooks();
1613
+ const blockedUsers = useCallBlockedUserIds();
1614
+ const blockedUsersSearchFn = react.useCallback((queryString) => {
1615
+ const queryRegExp = new RegExp(queryString, 'i');
1616
+ return Promise.resolve(blockedUsers.filter((blockedUser) => {
1617
+ return blockedUser.match(queryRegExp);
1618
+ }));
1619
+ }, [blockedUsers]);
1620
+ const { searchQueryInProgress, searchResults } = useSearch({
1621
+ searchFn: blockedUsersSearchFnFromProps ?? blockedUsersSearchFn,
1622
+ debounceInterval: debounceSearchInterval,
1623
+ searchQuery,
1624
+ });
1625
+ return (jsxRuntime.jsx(SearchResults, { EmptySearchResultComponent: EmptyParticipantSearchList, LoadingIndicator: LoadingIndicator, searchQueryInProgress: searchQueryInProgress, searchResults: searchQuery ? searchResults : blockedUsers, SearchResultList: BlockedUserListing }));
1626
+ };
1627
+ const ToggleButton$1 = react.forwardRef((props, ref) => {
1628
+ return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "filter", ref: ref });
1629
+ });
1630
+ const InviteLinkButton = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsxs("button", { ...props, className: clsx.clsx('str-video__invite-link-button', className), ref: ref, children: [jsxRuntime.jsx("div", { className: "str-video__invite-participant-icon" }), jsxRuntime.jsx("div", { className: "str-video__invite-link-button__text", children: "Invite Link" })] })));
1631
+
1632
+ const CallPreview = (props) => {
1633
+ const { className, style } = props;
1634
+ const call = videoReactBindings.useCall();
1635
+ const { useCallThumbnail } = videoReactBindings.useCallStateHooks();
1636
+ const thumbnail = useCallThumbnail();
1637
+ const [imageRef, setImageRef] = react.useState(null);
1638
+ react.useEffect(() => {
1639
+ if (!imageRef || !call)
1640
+ return;
1641
+ const cleanup = call.bindCallThumbnailElement(imageRef);
1642
+ return () => cleanup();
1643
+ }, [imageRef, call]);
1644
+ if (!thumbnail)
1645
+ return null;
1646
+ return (jsxRuntime.jsx("img", { className: clsx('str-video__call-preview', className), style: style, alt: "Call Preview Thumbnail", ref: setImageRef }));
1647
+ };
1648
+
1649
+ const CallRecordingListHeader = ({ callRecordings, onRefresh, }) => {
1650
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-recording-list__header", children: [jsxRuntime.jsxs("div", { className: "str-video__call-recording-list__title", children: [jsxRuntime.jsx("span", { children: "Call Recordings" }), callRecordings.length ? jsxRuntime.jsxs("span", { children: ["(", callRecordings.length, ")"] }) : null] }), jsxRuntime.jsx(IconButton, { icon: "refresh", title: "Refresh", onClick: onRefresh })] }));
1651
+ };
1652
+
1653
+ const CallRecordingListItem = ({ recording, }) => {
1654
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-recording-list-item", children: [jsxRuntime.jsx("div", { className: "str-video__call-recording-list-item__info", children: jsxRuntime.jsx("div", { className: "str-video__call-recording-list-item__created", children: new Date(recording.end_time).toLocaleString() }) }), jsxRuntime.jsxs("div", { className: "str-video__call-recording-list-item__actions", children: [jsxRuntime.jsx("a", { className: clsx('str-video__call-recording-list-item__action-button', 'str-video__call-recording-list-item__action-button--download'), role: "button", href: recording.url, download: recording.filename, title: "Download the recording", children: jsxRuntime.jsx("span", { className: clsx('str-video__call-recording-list-item__action-button-icon', 'str-video__download-button--icon') }) }), jsxRuntime.jsx(CopyToClipboardButtonWithPopup, { Button: CopyUrlButton, copyValue: recording.url })] })] }));
1655
+ };
1656
+ const CopyUrlButton = react.forwardRef((props, ref) => {
1657
+ return (jsxRuntime.jsx("button", { ...props, className: clsx('str-video__call-recording-list-item__action-button', 'str-video__call-recording-list-item__action-button--copy-link'), ref: ref, title: "Copy the recording link", children: jsxRuntime.jsx("span", { className: clsx('str-video__call-recording-list-item__action-button-icon', 'str-video__copy-button--icon') }) }));
1658
+ });
1659
+
1660
+ const EmptyCallRecordingListing = () => {
1661
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-recording-list__listing str-video__call-recording-list__listing--empty", children: [jsxRuntime.jsx("div", { className: "str-video__call-recording-list__listing--icon-empty" }), jsxRuntime.jsx("p", { className: "str-video__call-recording-list__listing--text-empty", children: "No recordings available" })] }));
1662
+ };
1663
+
1664
+ const LoadingCallRecordingListing = ({ callRecordings, }) => {
1665
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [callRecordings.map((recording) => (jsxRuntime.jsx(CallRecordingListItem, { recording: recording }, recording.filename))), jsxRuntime.jsx(LoadingIndicator, { text: "Recording getting ready" })] }));
1666
+ };
1667
+
1668
+ const CallRecordingList = ({ callRecordings, CallRecordingListHeader: CallRecordingListHeader$1 = CallRecordingListHeader, CallRecordingListItem: CallRecordingListItem$1 = CallRecordingListItem, EmptyCallRecordingList = EmptyCallRecordingListing, loading, LoadingCallRecordingList = LoadingCallRecordingListing, onRefresh, }) => {
1669
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-recording-list", children: [jsxRuntime.jsx(CallRecordingListHeader$1, { callRecordings: callRecordings, onRefresh: onRefresh }), jsxRuntime.jsx("div", { className: "str-video__call-recording-list__listing", children: loading ? (jsxRuntime.jsx(LoadingCallRecordingList, { callRecordings: callRecordings })) : callRecordings.length ? (callRecordings.map((recording) => (jsxRuntime.jsx(CallRecordingListItem$1, { recording: recording }, recording.filename)))) : (jsxRuntime.jsx(EmptyCallRecordingList, {})) })] }));
1670
+ };
1671
+
1672
+ const RingingCallControls = () => {
1673
+ const call = videoReactBindings.useCall();
1674
+ const { useCallCallingState } = videoReactBindings.useCallStateHooks();
1675
+ const callCallingState = useCallCallingState();
1676
+ if (!call)
1677
+ return null;
1678
+ const buttonsDisabled = callCallingState !== videoClient.CallingState.RINGING;
1679
+ return (jsxRuntime.jsx("div", { className: "str-video__pending-call-controls", children: call.isCreatedByMe ? (jsxRuntime.jsx(CancelCallButton, { disabled: buttonsDisabled })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(AcceptCallButton, { disabled: buttonsDisabled }), jsxRuntime.jsx(CancelCallButton, { onClick: () => call.leave({ reject: true }), disabled: buttonsDisabled })] })) }));
1680
+ };
1681
+
1682
+ const CALLING_STATE_TO_LABEL = {
1683
+ [videoClient.CallingState.JOINING]: 'Joining',
1684
+ [videoClient.CallingState.RINGING]: 'Ringing',
1685
+ [videoClient.CallingState.MIGRATING]: 'Migrating',
1686
+ [videoClient.CallingState.RECONNECTING]: 'Re-connecting',
1687
+ [videoClient.CallingState.RECONNECTING_FAILED]: 'Failed',
1688
+ [videoClient.CallingState.OFFLINE]: 'No internet connection',
1689
+ [videoClient.CallingState.IDLE]: '',
1690
+ [videoClient.CallingState.UNKNOWN]: '',
1691
+ [videoClient.CallingState.JOINED]: 'Joined',
1692
+ [videoClient.CallingState.LEFT]: 'Left call',
1693
+ };
1694
+ const RingingCall = (props) => {
1695
+ const { includeSelf = false, totalMembersToShow = 3 } = props;
1696
+ const call = videoReactBindings.useCall();
1697
+ const { t } = videoReactBindings.useI18n();
1698
+ const { useCallCallingState, useCallMembers } = videoReactBindings.useCallStateHooks();
1699
+ const callingState = useCallCallingState();
1700
+ const members = useCallMembers();
1701
+ const connectedUser = videoReactBindings.useConnectedUser();
1702
+ if (!call)
1703
+ return null;
1704
+ // take the first N members to show their avatars
1705
+ const membersToShow = (members || [])
1706
+ .slice(0, totalMembersToShow)
1707
+ .map(({ user }) => user)
1708
+ .filter((user) => user.id !== connectedUser?.id || includeSelf);
1709
+ if (includeSelf &&
1710
+ !membersToShow.find((user) => user.id === connectedUser?.id)) {
1711
+ // if the current user is not in the initial batch of members,
1712
+ // replace the first item in membersToShow array with the current user
1713
+ const self = members.find(({ user }) => user.id === connectedUser?.id);
1714
+ if (self) {
1715
+ membersToShow.splice(0, 1, self.user);
1716
+ }
1717
+ }
1718
+ const callingStateLabel = CALLING_STATE_TO_LABEL[callingState];
1719
+ return (jsxRuntime.jsxs("div", { className: "str-video__call-panel str-video__call-panel--ringing", children: [jsxRuntime.jsx("div", { className: "str-video__call-panel__members-list", children: membersToShow.map((user) => (jsxRuntime.jsxs("div", { className: "str-video__call-panel__member-box", children: [jsxRuntime.jsx(Avatar, { name: user.name, imageSrc: user.image }), user.name && (jsxRuntime.jsx("div", { className: "str-video__member_details", children: jsxRuntime.jsx("span", { className: "str-video__member_name", children: user.name }) }))] }, user.id))) }), callingStateLabel && (jsxRuntime.jsx("div", { className: "str-video__call-panel__calling-state-label", children: t(callingStateLabel) })), [videoClient.CallingState.RINGING, videoClient.CallingState.JOINING].includes(callingState) && (jsxRuntime.jsx(RingingCallControls, {}))] }));
1720
+ };
1721
+
1722
+ const byNameOrId = (a, b) => {
1723
+ if (a.name && b.name && a.name < b.name)
1724
+ return -1;
1725
+ if (a.name && b.name && a.name > b.name)
1726
+ return 1;
1727
+ if (a.id < b.id)
1728
+ return -1;
1729
+ if (a.id > b.id)
1730
+ return 1;
1731
+ return 0;
1732
+ };
1733
+ const PermissionRequests = () => {
1734
+ const call = videoReactBindings.useCall();
1735
+ const { useLocalParticipant } = videoReactBindings.useCallStateHooks();
1736
+ const localParticipant = useLocalParticipant();
1737
+ const [expanded, setExpanded] = react.useState(false);
1738
+ const [permissionRequests, setPermissionRequests] = react.useState([]);
1739
+ const canUpdateCallPermissions = videoReactBindings.useHasPermissions(videoClient.OwnCapability.UPDATE_CALL_PERMISSIONS);
1740
+ react.useEffect(() => {
1741
+ if (!call || !canUpdateCallPermissions)
1742
+ return;
1743
+ const unsubscribe = call.on('call.permission_request', (event) => {
1744
+ if (event.type !== 'call.permission_request')
1745
+ return;
1746
+ if (event.user.id !== localParticipant?.userId) {
1747
+ setPermissionRequests((requests) => [...requests, event].sort((a, b) => byNameOrId(a.user, b.user)));
1748
+ }
1749
+ });
1750
+ return () => {
1751
+ unsubscribe();
1752
+ };
1753
+ }, [call, canUpdateCallPermissions, localParticipant]);
1754
+ const handleUpdatePermission = (request, type) => {
1755
+ return async () => {
1756
+ const { user, permissions } = request;
1757
+ switch (type) {
1758
+ case 'grant':
1759
+ await call?.grantPermissions(user.id, permissions);
1760
+ break;
1761
+ case 'revoke':
1762
+ await call?.revokePermissions(user.id, permissions);
1763
+ break;
1764
+ }
1765
+ setPermissionRequests((requests) => requests.filter((r) => r !== request));
1766
+ };
1767
+ };
1768
+ const { refs, x, y, strategy } = useFloatingUIPreset({
1769
+ placement: 'bottom',
1770
+ strategy: 'absolute',
1771
+ });
1772
+ // don't render anything if there are no permission requests
1773
+ if (permissionRequests.length === 0)
1774
+ return null;
1775
+ 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: () => {
1776
+ setExpanded((e) => !e);
1777
+ }, children: expanded ? 'Hide requests' : 'Show requests' })] }), expanded && (jsxRuntime.jsx(PermissionRequestList, { ref: refs.setFloating, style: {
1778
+ position: strategy,
1779
+ top: y ?? 0,
1780
+ left: x ?? 0,
1781
+ overflowY: 'auto',
1782
+ }, permissionRequests: permissionRequests, handleUpdatePermission: handleUpdatePermission }))] }));
1783
+ };
1784
+ const PermissionRequestList = react.forwardRef((props, ref) => {
1785
+ const { permissionRequests, handleUpdatePermission, ...rest } = props;
1786
+ const { t } = videoReactBindings.useI18n();
1787
+ return (jsxRuntime.jsx("div", { className: "str-video__permission-requests-list", ref: ref, ...rest, children: permissionRequests.map((request, reqIndex) => {
1788
+ const { user, permissions } = request;
1789
+ 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}`));
1790
+ }) }));
1791
+ });
1792
+ const Button = (props) => {
1793
+ const { className, ...rest } = props;
1794
+ return (jsxRuntime.jsx("button", { className: clsx('str-video__permission-request__button', className), ...rest }));
1795
+ };
1796
+ const messageForPermission = (userName, permission, t) => {
1797
+ switch (permission) {
1798
+ case videoClient.OwnCapability.SEND_AUDIO:
1799
+ return t('{{ userName }} is requesting to speak', { userName });
1800
+ case videoClient.OwnCapability.SEND_VIDEO:
1801
+ return t('{{ userName }} is requesting to share their camera', {
1802
+ userName,
1803
+ });
1804
+ case videoClient.OwnCapability.SCREENSHARE:
1805
+ return t('{{ userName }} is requesting to present their screen', {
1806
+ userName,
1807
+ });
1808
+ default:
1809
+ return t('{{ userName }} is requesting permission: {{ permission }}', {
1810
+ userName,
1811
+ permission,
1812
+ });
1813
+ }
1814
+ };
1815
+
1816
+ const StreamTheme = ({ as: Component = 'div', className, children, ...props }) => {
1817
+ return (jsxRuntime.jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
1818
+ };
1819
+
1820
+ const DefaultDisabledVideoPreview = () => {
1821
+ return jsxRuntime.jsx("div", { children: "Video is disabled" });
1822
+ };
1823
+ const DefaultNoCameraPreview = () => {
1824
+ return jsxRuntime.jsx("div", { children: "No camera found" });
1825
+ };
1826
+ const DefaultVideoErrorPreview = ({ message }) => {
1827
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { children: "Error:" }), jsxRuntime.jsx("p", { children: message || 'Unexpected error happened' })] }));
1828
+ };
1829
+ const VideoPreview = ({ mirror = true, DisabledVideoPreview = DefaultDisabledVideoPreview, NoCameraPreview = DefaultNoCameraPreview, StartingCameraPreview = LoadingIndicator, VideoErrorPreview = DefaultVideoErrorPreview, }) => {
1830
+ const [stream, setStream] = react.useState();
1831
+ const { selectedVideoDeviceId, getVideoStream, initialVideoState, setInitialVideoState, } = useMediaDevices();
1832
+ // When there are 0 video devices (e.g. when laptop lid closed),
1833
+ // we do not restart the video automatically when the device is again available,
1834
+ // but rather leave turning the video on manually to the user.
1835
+ useOnUnavailableVideoDevices(() => setInitialVideoState(DEVICE_STATE.stopped));
1836
+ const videoDevices = useVideoDevices();
1837
+ react.useEffect(() => {
1838
+ if (!initialVideoState.enabled)
1839
+ return;
1840
+ getVideoStream({ deviceId: selectedVideoDeviceId })
1841
+ .then((s) => {
1842
+ setStream((previousStream) => {
1843
+ if (previousStream) {
1844
+ videoClient.disposeOfMediaStream(previousStream);
1845
+ }
1846
+ return s;
1847
+ });
1848
+ })
1849
+ .catch((e) => setInitialVideoState({
1850
+ ...DEVICE_STATE.error,
1851
+ message: e.message,
1852
+ }));
1853
+ return () => {
1854
+ setStream(undefined);
1855
+ };
1856
+ }, [
1857
+ initialVideoState,
1858
+ getVideoStream,
1859
+ selectedVideoDeviceId,
1860
+ setInitialVideoState,
1861
+ videoDevices.length,
1862
+ ]);
1863
+ react.useEffect(() => {
1864
+ if (initialVideoState.type === 'stopped') {
1865
+ setStream(undefined);
1866
+ }
1867
+ }, [initialVideoState]);
1868
+ const handleOnPlay = react.useCallback(() => {
1869
+ setInitialVideoState(DEVICE_STATE.playing);
1870
+ }, [setInitialVideoState]);
1871
+ let contents;
1872
+ if (initialVideoState.type === 'error') {
1873
+ contents = jsxRuntime.jsx(VideoErrorPreview, {});
1874
+ }
1875
+ else if (initialVideoState.type === 'stopped' && !videoDevices.length) {
1876
+ contents = jsxRuntime.jsx(NoCameraPreview, {});
1877
+ }
1878
+ else if (initialVideoState.enabled) {
1879
+ const loading = initialVideoState.type === 'starting';
1880
+ contents = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [stream && (jsxRuntime.jsx(BaseVideo, { stream: stream, className: clsx('str-video__video-preview', {
1881
+ 'str-video__video-preview--mirror': mirror,
1882
+ 'str-video__video-preview--loading': loading,
1883
+ }), onPlay: handleOnPlay })), loading && jsxRuntime.jsx(StartingCameraPreview, {})] }));
1884
+ }
1885
+ else {
1886
+ contents = jsxRuntime.jsx(DisabledVideoPreview, {});
1887
+ }
1888
+ return (jsxRuntime.jsx("div", { className: clsx('str-video__video-preview-container'), children: contents }));
1889
+ };
1890
+
1891
+ const DebugParticipantPublishQuality = (props) => {
1892
+ const { call, participant } = props;
1893
+ const [quality, setQuality] = react.useState();
1894
+ const [publishStats, setPublishStats] = react.useState(() => ({
1895
+ f: true,
1896
+ h: true,
1897
+ q: true,
1898
+ }));
1899
+ react.useEffect(() => {
1900
+ return call.on('changePublishQuality', (event) => {
1901
+ if (event.eventPayload.oneofKind !== 'changePublishQuality')
1902
+ return;
1903
+ const { videoSenders } = event.eventPayload.changePublishQuality;
1904
+ // FIXME OL: support additional layers (like screenshare)
1905
+ const [videoLayer] = videoSenders.map(({ layers }) => {
1906
+ return layers.map((l) => ({ [l.name]: l.active }));
1907
+ });
1908
+ setPublishStats((s) => ({
1909
+ ...s,
1910
+ ...videoLayer,
1911
+ }));
1912
+ });
1913
+ }, [call]);
1914
+ return (jsxRuntime.jsxs("select", { title: `Published tracks: ${JSON.stringify(publishStats)}`, value: quality, onChange: (e) => {
1915
+ const value = e.target.value;
1916
+ setQuality(value);
1917
+ let w = 960;
1918
+ let h = 540;
1919
+ if (value === 'h') {
1920
+ w = w / 2; // 480
1921
+ h = h / 2; // 270
1922
+ }
1923
+ else if (value === 'q') {
1924
+ w = w / 4; // 240
1925
+ h = h / 4; // 135
1926
+ }
1927
+ call.updateSubscriptionsPartial('video', {
1928
+ [participant.sessionId]: {
1929
+ dimension: {
1930
+ width: w,
1931
+ height: h,
1932
+ },
1933
+ },
1934
+ });
1935
+ }, children: [jsxRuntime.jsx("option", { value: "f", children: "High (f)" }), jsxRuntime.jsx("option", { value: "h", children: "Medium (h)" }), jsxRuntime.jsx("option", { value: "q", children: "Low (q)" })] }));
1936
+ };
1937
+
1938
+ const DebugStatsView = (props) => {
1939
+ const { call, mediaStream, sessionId, userId } = props;
1940
+ const { useCallStatsReport } = videoReactBindings.useCallStateHooks();
1941
+ const callStatsReport = useCallStatsReport();
1942
+ react.useEffect(() => {
1943
+ call.startReportingStatsFor(sessionId);
1944
+ return () => {
1945
+ call.stopReportingStatsFor(sessionId);
1946
+ };
1947
+ }, [call, sessionId]);
1948
+ const reportForTracks = callStatsReport?.participants[sessionId];
1949
+ const trackStats = reportForTracks?.flatMap((report) => report.streams);
1950
+ const previousWidth = react.useRef({ f: 0, h: 0, q: 0 });
1951
+ const previousHeight = react.useRef({ f: 0, h: 0, q: 0 });
1952
+ trackStats?.forEach((track) => {
1953
+ if (track.kind !== 'video')
1954
+ return;
1955
+ const { frameWidth = 0, frameHeight = 0, rid = '' } = track;
1956
+ if (frameWidth !== previousWidth.current[rid] ||
1957
+ frameHeight !== previousHeight.current[rid]) {
1958
+ const trackSize = `${frameWidth}x${frameHeight}`;
1959
+ console.log(`Track stats (${userId}/${sessionId}): ${rid}(${trackSize})`);
1960
+ previousWidth.current[rid] = frameWidth;
1961
+ previousHeight.current[rid] = frameHeight;
1962
+ }
1963
+ });
1964
+ const { refs, strategy, y, x } = useFloatingUIPreset({
1965
+ placement: 'top',
1966
+ strategy: 'absolute',
1967
+ });
1968
+ const [isPopperOpen, setIsPopperOpen] = react.useState(false);
1969
+ const [videoTrack] = mediaStream?.getVideoTracks() ?? [];
1970
+ const settings = videoTrack?.getSettings();
1971
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "str-video__debug__track-stats-icon", tabIndex: 0, ref: refs.setReference, title: settings &&
1972
+ `${settings.width}x${settings.height}@${Math.round(settings.frameRate || 0)}`, onClick: () => {
1973
+ setIsPopperOpen((v) => !v);
1974
+ } }), isPopperOpen && (jsxRuntime.jsxs("div", { className: "str-video__debug__track-stats str-video__call-stats", ref: refs.setFloating, style: {
1975
+ position: strategy,
1976
+ top: y ?? 0,
1977
+ left: x ?? 0,
1978
+ overflowY: 'auto',
1979
+ }, children: [jsxRuntime.jsx("h3", { children: "Participant stats" }), jsxRuntime.jsx("div", { className: "str-video__call-stats__card-container", children: trackStats
1980
+ ?.map((track) => {
1981
+ if (track.kind === 'video') {
1982
+ return (jsxRuntime.jsx(StatCard, { label: `${track.kind}: ${track.codec} ` +
1983
+ (track.rid ? ` (${track.rid})` : ''), value: `${track.frameWidth || 0}x${track.frameHeight || 0}@${track.framesPerSecond || 0}fps` }, `${track.rid}/${track.ssrc}/${track.codec}/${track.kind}`));
1984
+ }
1985
+ else if (track.kind === 'audio') {
1986
+ return (jsxRuntime.jsx(StatCard, { label: track.codec || 'N/A', value: `Jitter: ${track.jitter || 0}ms` }, `${track.ssrc}/${track.codec}/${track.kind}`));
1987
+ }
1988
+ return null;
1989
+ })
1990
+ .filter(Boolean) }), reportForTracks?.map((report, index) => (jsxRuntime.jsx("pre", { children: JSON.stringify(unwrapStats(report.rawStats), null, 2) }, index)))] }))] }));
1991
+ };
1992
+ const unwrapStats = (rawStats) => {
1993
+ const decodedStats = {};
1994
+ rawStats?.forEach((s) => {
1995
+ decodedStats[s.id] = s;
1996
+ });
1997
+ return decodedStats;
1998
+ };
1999
+
2000
+ const ToggleButton = react.forwardRef((props, ref) => {
2001
+ return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
2002
+ });
2003
+ const DefaultScreenShareOverlay = () => {
2004
+ const call = videoReactBindings.useCall();
2005
+ const stopScreenShare = () => {
2006
+ call?.stopPublish(videoClient.SfuModels.TrackType.SCREEN_SHARE).catch(console.error);
2007
+ };
2008
+ 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: "You are presenting your screen" }), jsxRuntime.jsxs("button", { onClick: stopScreenShare, className: "str-video__screen-share-overlay__button", children: [jsxRuntime.jsx(Icon, { icon: "close" }), " Stop Screen Sharing"] })] }));
2009
+ };
2010
+ const DefaultParticipantViewUI = ({ indicatorsVisible = true, menuPlacement = 'bottom-end', showMenuButton = true, }) => {
2011
+ const { participant, participantViewElement, trackType, videoElement } = useParticipantViewContext();
2012
+ const { publishedTracks } = participant;
2013
+ const hasScreenShare = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2014
+ if (participant.isLocalParticipant &&
2015
+ hasScreenShare &&
2016
+ trackType === 'screenShareTrack') {
2017
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DefaultScreenShareOverlay, {}), jsxRuntime.jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
2018
+ }
2019
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [showMenuButton && (jsxRuntime.jsx(MenuToggle, { strategy: "fixed", placement: menuPlacement, ToggleButton: ToggleButton, children: jsxRuntime.jsx(ParticipantActionsContextMenu, { participantViewElement: participantViewElement, participant: participant, videoElement: videoElement }) })), jsxRuntime.jsx(Reaction, { participant: participant }), jsxRuntime.jsx(ParticipantDetails, { indicatorsVisible: indicatorsVisible })] }));
2020
+ };
2021
+ const ParticipantDetails = ({ indicatorsVisible = true, }) => {
2022
+ const { participant } = useParticipantViewContext();
2023
+ const { isDominantSpeaker, isLocalParticipant, connectionQuality, publishedTracks, pin, sessionId, name, userId, videoStream, } = participant;
2024
+ const call = videoReactBindings.useCall();
2025
+ const connectionQualityAsString = !!connectionQuality &&
2026
+ videoClient.SfuModels.ConnectionQuality[connectionQuality].toLowerCase();
2027
+ const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
2028
+ const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
2029
+ const canUnpin = !!pin && pin.isLocalPin;
2030
+ const isDebugMode = useIsDebugMode();
2031
+ return (jsxRuntime.jsxs("div", { className: "str-video__participant-details", children: [jsxRuntime.jsxs("span", { className: "str-video__participant-details__name", children: [name || userId, indicatorsVisible && isDominantSpeaker && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--dominant_speaker", title: "Dominant speaker" })), indicatorsVisible && (jsxRuntime.jsx(Notification, { isVisible: isLocalParticipant &&
2032
+ connectionQuality === videoClient.SfuModels.ConnectionQuality.POOR, message: "Poor connection quality. Please check your internet connection.", children: connectionQualityAsString && (jsxRuntime.jsx("span", { className: clsx.clsx('str-video__participant-details__connection-quality', `str-video__participant-details__connection-quality--${connectionQualityAsString}`), title: connectionQualityAsString })) })), indicatorsVisible && !hasAudio && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--audio-muted" })), indicatorsVisible && !hasVideo && (jsxRuntime.jsx("span", { className: "str-video__participant-details__name--video-muted" })), indicatorsVisible && canUnpin && (
2033
+ // TODO: remove this monstrosity once we have a proper design
2034
+ jsxRuntime.jsx("span", { title: "Unpin", onClick: () => call?.unpin(sessionId), style: { cursor: 'pointer' }, className: "str-video__participant-details__name--pinned" }))] }), isDebugMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(DebugParticipantPublishQuality, { participant: participant, call: call }), jsxRuntime.jsx(DebugStatsView, { call: call, sessionId: sessionId, userId: userId, mediaStream: videoStream })] }))] }));
2035
+ };
2036
+
2037
+ const ParticipantViewContext = react.createContext(undefined);
2038
+ const useParticipantViewContext = () => react.useContext(ParticipantViewContext);
2039
+ const ParticipantView = react.forwardRef(({ participant, trackType = 'videoTrack', muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) => {
2040
+ const { isLocalParticipant, isSpeaking, isDominantSpeaker, publishedTracks, sessionId, } = participant;
2041
+ const hasAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.AUDIO);
2042
+ const hasVideo = publishedTracks.includes(videoClient.SfuModels.TrackType.VIDEO);
2043
+ const hasScreenShareAudio = publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE_AUDIO);
2044
+ const [trackedElement, setTrackedElement] = react.useState(null);
2045
+ const [contextVideoElement, setContextVideoElement] = react.useState(null);
2046
+ const [contextVideoPlaceholderElement, setContextVideoPlaceholderElement] = react.useState(null);
2047
+ // TODO: allow to pass custom ViewportTracker instance from props
2048
+ useTrackElementVisibility({
2049
+ sessionId,
2050
+ trackedElement,
2051
+ trackType,
2052
+ });
2053
+ const participantViewContextValue = react.useMemo(() => ({
2054
+ participant,
2055
+ participantViewElement: trackedElement,
2056
+ videoElement: contextVideoElement,
2057
+ videoPlaceholderElement: contextVideoPlaceholderElement,
2058
+ trackType,
2059
+ }), [
2060
+ contextVideoElement,
2061
+ contextVideoPlaceholderElement,
2062
+ participant,
2063
+ trackedElement,
2064
+ trackType,
2065
+ ]);
2066
+ const videoRefs = react.useMemo(() => ({
2067
+ setVideoElement: (element) => {
2068
+ setVideoElement?.(element);
2069
+ setContextVideoElement(element);
2070
+ },
2071
+ setVideoPlaceholderElement: (element) => {
2072
+ setVideoPlaceholderElement?.(element);
2073
+ setContextVideoPlaceholderElement(element);
2074
+ },
2075
+ }), [setVideoElement, setVideoPlaceholderElement]);
2076
+ return (jsxRuntime.jsx("div", { "data-testid": "participant-view", ref: (element) => {
2077
+ applyElementToRef(ref, element);
2078
+ setTrackedElement(element);
2079
+ }, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideo && 'str-video__participant-view--no-video', !hasAudio && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudio && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudio && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2080
+ });
2081
+
2082
+ const DEVICE_STATE_TOGGLE = {
2083
+ starting: 'stopped',
2084
+ playing: 'stopped',
2085
+ stopped: 'starting',
2086
+ uninitialized: 'starting',
2087
+ error: 'starting',
2088
+ };
2089
+ /**
2090
+ * Exclude types from documentation site, but we should still add doc comments
2091
+ * @internal
2092
+ */
2093
+ const DEVICE_STATE = {
2094
+ starting: { type: 'starting', enabled: true },
2095
+ playing: { type: 'playing', enabled: true },
2096
+ stopped: { type: 'stopped', enabled: false },
2097
+ uninitialized: { type: 'uninitialized', enabled: false },
2098
+ error: { type: 'error', message: '', enabled: false },
2099
+ };
2100
+ const DEFAULT_DEVICE_ID = 'default';
2101
+ const MediaDevicesContext = react.createContext(null);
2102
+ /**
2103
+ * Context provider that internally puts in place mechanisms that:
2104
+ * 1. fall back to selecting a default device when trying to switch to a non-existent device
2105
+ * 2. fall back to a default device when an active device is disconnected
2106
+ * 3. stop publishing a media stream when a non-default device is disconnected
2107
+ * 4. republish a media stream from the newly connected default device
2108
+ * 5. republish a media stream when a new device is selected
2109
+ *
2110
+ * Provides `MediaDevicesContextAPI` that allow the integrators to handle:
2111
+ * 1. the initial device state enablement (for example apt for lobby scenario)
2112
+ * 2. media stream retrieval and disposal
2113
+ * 3. media stream publishing
2114
+ * 4. specific device selection
2115
+ * @param params
2116
+ * @returns
2117
+ *
2118
+ * @category Device Management
2119
+ */
2120
+ const MediaDevicesProvider = ({ children, initialAudioEnabled, initialVideoEnabled, initialVideoInputDeviceId = DEFAULT_DEVICE_ID, initialAudioOutputDeviceId = DEFAULT_DEVICE_ID, initialAudioInputDeviceId = DEFAULT_DEVICE_ID, }) => {
2121
+ const call = videoReactBindings.useCall();
2122
+ const { useCallCallingState, useCallState, useCallSettings } = videoReactBindings.useCallStateHooks();
2123
+ const callingState = useCallCallingState();
2124
+ const callState = useCallState();
2125
+ const { localParticipant$ } = callState;
2126
+ const hasBrowserPermissionVideoInput = useHasBrowserPermissions('camera');
2127
+ const hasBrowserPermissionAudioInput = useHasBrowserPermissions('microphone');
2128
+ const [selectedAudioInputDeviceId, selectAudioInputDeviceId] = react.useState(initialAudioInputDeviceId);
2129
+ const [selectedAudioOutputDeviceId, selectAudioOutputDeviceId] = react.useState(initialAudioOutputDeviceId);
2130
+ const [selectedVideoDeviceId, selectVideoDeviceId] = react.useState(initialVideoInputDeviceId);
2131
+ const [isAudioOutputChangeSupported] = react.useState(() => videoClient.checkIfAudioOutputChangeSupported());
2132
+ const [initAudioEnabled, setInitialAudioEnabled] = react.useState(!!initialAudioEnabled);
2133
+ const [initialVideoState, setInitialVideoState] = react.useState(() => initialVideoEnabled ? DEVICE_STATE.starting : DEVICE_STATE.uninitialized);
2134
+ const settings = useCallSettings();
2135
+ react.useEffect(() => {
2136
+ if (!settings)
2137
+ return;
2138
+ const { audio, video } = settings;
2139
+ if (typeof initialAudioEnabled === 'undefined' && audio.mic_default_on) {
2140
+ setInitialAudioEnabled(audio.mic_default_on);
2141
+ }
2142
+ if (typeof initialVideoEnabled === 'undefined' && video.camera_default_on) {
2143
+ setInitialVideoState(DEVICE_STATE.starting);
2144
+ }
2145
+ }, [initialAudioEnabled, initialVideoEnabled, settings]);
2146
+ const publishVideoStream = useVideoPublisher({
2147
+ initialVideoMuted: !initialVideoState.enabled,
2148
+ videoDeviceId: selectedVideoDeviceId,
2149
+ });
2150
+ const publishAudioStream = useAudioPublisher({
2151
+ initialAudioMuted: !initAudioEnabled,
2152
+ audioDeviceId: selectedAudioInputDeviceId,
2153
+ });
2154
+ const stopPublishingAudio = react.useCallback(async () => {
2155
+ if (callingState === videoClient.CallingState.IDLE ||
2156
+ callingState === videoClient.CallingState.RINGING) {
2157
+ setInitialAudioEnabled(false);
2158
+ }
2159
+ else {
2160
+ call?.stopPublish(videoClient.SfuModels.TrackType.AUDIO);
2161
+ }
2162
+ }, [call, callingState]);
2163
+ const stopPublishingVideo = react.useCallback(async () => {
2164
+ if (callingState === videoClient.CallingState.IDLE ||
2165
+ callingState === videoClient.CallingState.RINGING) {
2166
+ setInitialVideoState(DEVICE_STATE.stopped);
2167
+ }
2168
+ else {
2169
+ call?.stopPublish(videoClient.SfuModels.TrackType.VIDEO);
2170
+ }
2171
+ }, [call, callingState]);
2172
+ const toggleInitialAudioMuteState = react.useCallback(() => setInitialAudioEnabled((prev) => !prev), []);
2173
+ const toggleInitialVideoMuteState = react.useCallback(() => setInitialVideoState((prev) => {
2174
+ const newType = DEVICE_STATE_TOGGLE[prev.type];
2175
+ return DEVICE_STATE[newType];
2176
+ }), []);
2177
+ const switchDevice = react.useCallback((kind, deviceId) => {
2178
+ if (kind === 'videoinput') {
2179
+ selectVideoDeviceId(deviceId);
2180
+ }
2181
+ if (kind === 'audioinput') {
2182
+ selectAudioInputDeviceId(deviceId);
2183
+ }
2184
+ if (kind === 'audiooutput') {
2185
+ selectAudioOutputDeviceId(deviceId);
2186
+ }
2187
+ }, []);
2188
+ useAudioInputDeviceFallback(() => switchDevice('audioinput', DEFAULT_DEVICE_ID), hasBrowserPermissionAudioInput, selectedAudioInputDeviceId);
2189
+ useAudioOutputDeviceFallback(() => switchDevice('audiooutput', DEFAULT_DEVICE_ID),
2190
+ // audiooutput devices can be enumerated only with microphone permissions
2191
+ hasBrowserPermissionAudioInput, selectedAudioOutputDeviceId);
2192
+ useVideoDeviceFallback(() => switchDevice('videoinput', DEFAULT_DEVICE_ID), hasBrowserPermissionVideoInput, selectedVideoDeviceId);
2193
+ react.useEffect(() => {
2194
+ if (!call || callingState !== videoClient.CallingState.JOINED)
2195
+ return;
2196
+ call.setAudioOutputDevice(selectedAudioOutputDeviceId);
2197
+ }, [call, callingState, selectedAudioOutputDeviceId]);
2198
+ react.useEffect(() => {
2199
+ // audiooutput devices can be enumerated only with microphone permissions
2200
+ if (!localParticipant$ || !hasBrowserPermissionAudioInput)
2201
+ return;
2202
+ const subscription = videoClient.watchForDisconnectedAudioOutputDevice(localParticipant$.pipe(rxjs.map((p) => p?.audioOutputDeviceId))).subscribe(async () => {
2203
+ selectAudioOutputDeviceId(DEFAULT_DEVICE_ID);
2204
+ });
2205
+ return () => {
2206
+ subscription.unsubscribe();
2207
+ };
2208
+ }, [hasBrowserPermissionAudioInput, localParticipant$]);
2209
+ const contextValue = {
2210
+ disposeOfMediaStream: videoClient.disposeOfMediaStream,
2211
+ getAudioStream: videoClient.getAudioStream,
2212
+ getVideoStream: videoClient.getVideoStream,
2213
+ isAudioOutputChangeSupported,
2214
+ selectedAudioInputDeviceId,
2215
+ selectedAudioOutputDeviceId,
2216
+ selectedVideoDeviceId,
2217
+ switchDevice,
2218
+ initialAudioEnabled: initAudioEnabled,
2219
+ initialVideoState,
2220
+ setInitialAudioEnabled,
2221
+ setInitialVideoState,
2222
+ toggleInitialAudioMuteState,
2223
+ toggleInitialVideoMuteState,
2224
+ publishAudioStream,
2225
+ publishVideoStream,
2226
+ stopPublishingAudio,
2227
+ stopPublishingVideo,
2228
+ };
2229
+ return (jsxRuntime.jsx(MediaDevicesContext.Provider, { value: contextValue, children: children }));
2230
+ };
2231
+ /**
2232
+ * Context consumer retrieving MediaDevicesContextAPI.
2233
+ * @returns
2234
+ *
2235
+ * @category Device Management
2236
+ */
2237
+ const useMediaDevices = () => {
2238
+ const value = react.useContext(MediaDevicesContext);
2239
+ if (!value) {
2240
+ console.warn(`Null MediaDevicesContext`);
2241
+ }
2242
+ return value;
2243
+ };
2244
+
2245
+ const StreamCall = ({ children, call, mediaDevicesProviderProps, }) => {
2246
+ return (jsxRuntime.jsx(videoReactBindings.StreamCallProvider, { call: call, children: jsxRuntime.jsx(MediaDevicesProvider, { ...mediaDevicesProviderProps, children: children }) }));
2247
+ };
2248
+
2249
+ var Joining = "Joining";
2250
+ var Mic = "Mic";
2251
+ var Ringing = "Ringing";
2252
+ var Speakers = "Speakers";
2253
+ var Video = "Video";
2254
+ var Live = "Live";
2255
+ var Reactions = "Reactions";
2256
+ var Invite = "Invite";
2257
+ var Join = "Join";
2258
+ var You = "You";
2259
+ var Me = "Me";
2260
+ var Unknown = "Unknown";
2261
+ var Allow = "Allow";
2262
+ var Revoke = "Revoke";
2263
+ var Dismiss = "Dismiss";
2264
+ var Pinned = "Pinned";
2265
+ var Unpin = "Unpin";
2266
+ var Pin = "Pin";
2267
+ var Block = "Block";
2268
+ var Enter = "Enter";
2269
+ var Leave = "Leave";
2270
+ var Participants = "Participants";
2271
+ var Anonymous = ", and ({{ count }}) anonymous";
2272
+ var en = {
2273
+ Joining: Joining,
2274
+ Mic: Mic,
2275
+ "No internet connection": "No internet connection",
2276
+ "Re-connecting": "Re-connecting",
2277
+ Ringing: Ringing,
2278
+ "Screen Share": "Screen Share",
2279
+ "Select a Camera": "Select a Camera",
2280
+ "Select a Mic": "Select a Mic",
2281
+ "Select Speakers": "Select Speakers",
2282
+ Speakers: Speakers,
2283
+ Video: Video,
2284
+ "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
2285
+ Live: Live,
2286
+ "You can now speak.": "You can now speak.",
2287
+ "Awaiting for an approval to speak.": "Awaiting for an approval to speak.",
2288
+ "You can no longer speak.": "You can no longer speak.",
2289
+ "You can now share your video.": "You can now share your video.",
2290
+ "Awaiting for an approval to share your video.": "Awaiting for an approval to share your video.",
2291
+ "You can no longer share your video.": "You can no longer share your video.",
2292
+ "Waiting for recording to stop...": "Waiting for recording to stop...",
2293
+ "Waiting for recording to start...": "Waiting for recording to start...",
2294
+ "Record call": "Record call",
2295
+ Reactions: Reactions,
2296
+ "You can now share your screen.": "You can now share your screen.",
2297
+ "Awaiting for an approval to share screen.": "Awaiting for an approval to share screen.",
2298
+ "You can no longer share your screen.": "You can no longer share your screen.",
2299
+ "Share screen": "Share screen",
2300
+ "Incoming Call...": "Incoming Call...",
2301
+ "Calling...": "Calling...",
2302
+ "Mute All": "Mute All",
2303
+ Invite: Invite,
2304
+ Join: Join,
2305
+ You: You,
2306
+ Me: Me,
2307
+ Unknown: Unknown,
2308
+ Allow: Allow,
2309
+ Revoke: Revoke,
2310
+ Dismiss: Dismiss,
2311
+ "Microphone on": "Microphone on",
2312
+ "Microphone off": "Microphone off",
2313
+ "Camera on": "Camera on",
2314
+ "Camera off": "Camera off",
2315
+ Pinned: Pinned,
2316
+ Unpin: Unpin,
2317
+ Pin: Pin,
2318
+ "Pin for everyone": "Pin for everyone",
2319
+ "Unpin for everyone": "Unpin for everyone",
2320
+ Block: Block,
2321
+ "Turn off video": "Turn off video",
2322
+ "Turn off screen share": "Turn off screen share",
2323
+ "Mute audio": "Mute audio",
2324
+ "Mute screen share audio": "Mute screen share audio",
2325
+ "Allow audio": "Allow audio",
2326
+ "Allow video": "Allow video",
2327
+ "Allow screen sharing": "Allow screen sharing",
2328
+ "Disable audio": "Disable audio",
2329
+ "Disable video": "Disable video",
2330
+ "Disable screen sharing": "Disable screen sharing",
2331
+ Enter: Enter,
2332
+ Leave: Leave,
2333
+ "{{ direction }} fullscreen": "{{ direction }} fullscreen",
2334
+ "{{ direction }} picture-in-picture": "{{ direction }} picture-in-picture",
2335
+ Participants: Participants,
2336
+ Anonymous: Anonymous,
2337
+ "No participants found": "No participants found",
2338
+ "Participants ({{ numberOfParticipants }})": "Participants ({{ numberOfParticipants }})",
2339
+ "{{ userName }} is sharing their screen": "{{ userName }} is sharing their screen",
2340
+ "{{ userName }} is requesting to speak": "{{ userName }} is requesting to speak",
2341
+ "{{ userName }} is requesting to share their camera": "{{ userName }} is requesting to share their camera",
2342
+ "{{ userName }} is requesting to present their screen": "{{ userName }} is requesting to present their screen",
2343
+ "{{ userName }} is requesting permission: {{ permission }}": "{{ userName }} is requesting permission: {{ permission }}"
2344
+ };
2345
+
2346
+ const translations = { en };
2347
+
2348
+ const StreamVideo = (props) => {
2349
+ return (jsxRuntime.jsx(videoReactBindings.StreamVideoProvider, { translationsOverrides: translations, ...props }));
2350
+ };
2351
+
2352
+ const usePaginatedLayoutSortPreset = (call) => {
2353
+ react.useEffect(() => {
2354
+ if (!call)
2355
+ return;
2356
+ call.setSortParticipantsBy(videoClient.paginatedLayoutSortPreset);
2357
+ return () => {
2358
+ resetSortPreset(call);
2359
+ };
2360
+ }, [call]);
2361
+ };
2362
+ const useSpeakerLayoutSortPreset = (call, isOneOnOneCall) => {
2363
+ react.useEffect(() => {
2364
+ if (!call)
2365
+ return;
2366
+ // always show the remote participant in the spotlight
2367
+ if (isOneOnOneCall) {
2368
+ call.setSortParticipantsBy(videoClient.combineComparators(videoClient.screenSharing, loggedIn));
2369
+ }
2370
+ else {
2371
+ call.setSortParticipantsBy(videoClient.speakerLayoutSortPreset);
2372
+ }
2373
+ return () => {
2374
+ resetSortPreset(call);
2375
+ };
2376
+ }, [call, isOneOnOneCall]);
2377
+ };
2378
+ const resetSortPreset = (call) => {
2379
+ // reset the sorting to the default for the call type
2380
+ const callConfig = videoClient.CallTypes.get(call.type);
2381
+ call.setSortParticipantsBy(callConfig.options.sortParticipantsBy || videoClient.defaultSortPreset);
2382
+ };
2383
+ const loggedIn = (a, b) => {
2384
+ if (a.isLocalParticipant)
2385
+ return 1;
2386
+ if (b.isLocalParticipant)
2387
+ return -1;
2388
+ return 0;
2389
+ };
2390
+
2391
+ const LivestreamLayout = (props) => {
2392
+ const { useParticipants, useRemoteParticipants, useHasOngoingScreenShare } = videoReactBindings.useCallStateHooks();
2393
+ const call = videoReactBindings.useCall();
2394
+ const [currentSpeaker, ...otherParticipants] = useParticipants();
2395
+ const remoteParticipants = useRemoteParticipants();
2396
+ const hasOngoingScreenShare = useHasOngoingScreenShare();
2397
+ const presenter = hasOngoingScreenShare
2398
+ ? hasScreenShare$1(currentSpeaker) && currentSpeaker
2399
+ : otherParticipants.find(hasScreenShare$1);
2400
+ usePaginatedLayoutSortPreset(call);
2401
+ const Overlay = (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName }));
2402
+ const { floatingParticipantProps } = props;
2403
+ const FloatingParticipantOverlay = hasOngoingScreenShare && (jsxRuntime.jsx(ParticipantOverlay
2404
+ // these elements aren't needed for the video feed
2405
+ , {
2406
+ // these elements aren't needed for the video feed
2407
+ showParticipantCount: floatingParticipantProps?.showParticipantCount ?? false, showDuration: floatingParticipantProps?.showDuration ?? false, showLiveBadge: floatingParticipantProps?.showLiveBadge ?? false, showSpeakerName: floatingParticipantProps?.showSpeakerName ?? true }));
2408
+ return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [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
2409
+ : true })), currentSpeaker && (jsxRuntime.jsx(ParticipantView, { className: clsx(hasOngoingScreenShare &&
2410
+ clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: FloatingParticipantOverlay || Overlay, muteAudio // audio is rendered by ParticipantsAudio
2411
+ : true }))] }));
2412
+ };
2413
+ const hasScreenShare$1 = (p) => !!p?.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2414
+ const ParticipantOverlay = (props) => {
2415
+ const { enableFullScreen = true, showParticipantCount = true, showDuration = true, showLiveBadge = true, showSpeakerName = false, } = props;
2416
+ const { participant } = useParticipantViewContext();
2417
+ const { useParticipantCount } = videoReactBindings.useCallStateHooks();
2418
+ const participantCount = useParticipantCount();
2419
+ const duration = useUpdateCallDuration();
2420
+ const toggleFullScreen = useToggleFullScreen();
2421
+ const { t } = videoReactBindings.useI18n();
2422
+ return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar", 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: participantCount })), showSpeakerName && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__speaker-name", title: participant.name || participant.userId || '', children: participant.name || participant.userId || '' })), showDuration && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__duration", children: formatDuration(duration) })), enableFullScreen && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__go-fullscreen", onClick: toggleFullScreen }))] }) }));
2423
+ };
2424
+ const useUpdateCallDuration = () => {
2425
+ const { useIsCallLive, useCallSession } = videoReactBindings.useCallStateHooks();
2426
+ const isCallLive = useIsCallLive();
2427
+ const session = useCallSession();
2428
+ const [duration, setDuration] = react.useState(() => {
2429
+ if (!session || !session.live_started_at)
2430
+ return 0;
2431
+ const liveStartTime = new Date(session.live_started_at);
2432
+ const now = new Date();
2433
+ return Math.floor((now.getTime() - liveStartTime.getTime()) / 1000);
2434
+ });
2435
+ react.useEffect(() => {
2436
+ if (!isCallLive)
2437
+ return;
2438
+ const interval = setInterval(() => {
2439
+ setDuration((d) => d + 1);
2440
+ }, 1000);
2441
+ return () => {
2442
+ clearInterval(interval);
2443
+ };
2444
+ }, [isCallLive]);
2445
+ return duration;
2446
+ };
2447
+ const useToggleFullScreen = () => {
2448
+ const { participantViewElement } = useParticipantViewContext();
2449
+ const [isFullscreen, setIsFullscreen] = react.useState(false);
2450
+ return react.useCallback(() => {
2451
+ if (isFullscreen) {
2452
+ document.exitFullscreen().then(() => {
2453
+ setIsFullscreen(false);
2454
+ });
2455
+ }
2456
+ else {
2457
+ participantViewElement?.requestFullscreen().then(() => {
2458
+ setIsFullscreen(true);
2459
+ });
2460
+ }
2461
+ }, [isFullscreen, participantViewElement]);
2462
+ };
2463
+ const formatDuration = (durationInMs) => {
2464
+ const days = Math.floor(durationInMs / 86400);
2465
+ const hours = Math.floor(durationInMs / 3600);
2466
+ const minutes = Math.floor((durationInMs % 3600) / 60);
2467
+ const seconds = durationInMs % 60;
2468
+ return `${days ? days + ' ' : ''}${hours ? hours + ':' : ''}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
2469
+ };
2470
+
2471
+ const GROUP_SIZE = 16;
2472
+ const PaginatedGridLayoutGroup = ({ group, VideoPlaceholder, ParticipantViewUI, }) => {
2473
+ return (jsxRuntime.jsx("div", { className: clsx('str-video__paginated-grid-layout__group', {
2474
+ 'str-video__paginated-grid-layout--one': group.length === 1,
2475
+ 'str-video__paginated-grid-layout--two-four': group.length >= 2 && group.length <= 4,
2476
+ 'str-video__paginated-grid-layout--five-nine': group.length >= 5 && group.length <= 9,
2477
+ }), children: group.map((participant) => (jsxRuntime.jsx(ParticipantView, { participant: participant, muteAudio: true, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI }, participant.sessionId))) }));
2478
+ };
2479
+ const PaginatedGridLayout = ({ groupSize = GROUP_SIZE, excludeLocalParticipant = false, pageArrowsVisible = true, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }) => {
2480
+ const [page, setPage] = react.useState(0);
2481
+ const [paginatedGridLayoutWrapperElement, setPaginatedGridLayoutWrapperElement,] = react.useState(null);
2482
+ const call = videoReactBindings.useCall();
2483
+ const { useParticipants, useRemoteParticipants } = videoReactBindings.useCallStateHooks();
2484
+ const participants = useParticipants();
2485
+ // used to render audio elements
2486
+ const remoteParticipants = useRemoteParticipants();
2487
+ usePaginatedLayoutSortPreset(call);
2488
+ react.useEffect(() => {
2489
+ if (!paginatedGridLayoutWrapperElement || !call)
2490
+ return;
2491
+ const cleanup = call.setViewport(paginatedGridLayoutWrapperElement);
2492
+ return () => cleanup();
2493
+ }, [paginatedGridLayoutWrapperElement, call]);
2494
+ // only used to render video elements
2495
+ const participantGroups = react.useMemo(() => chunk(excludeLocalParticipant ? remoteParticipants : participants, groupSize), [excludeLocalParticipant, remoteParticipants, participants, groupSize]);
2496
+ const pageCount = participantGroups.length;
2497
+ // update page when page count is reduced and selected page no longer exists
2498
+ react.useEffect(() => {
2499
+ if (page > pageCount - 1) {
2500
+ setPage(Math.max(0, pageCount - 1));
2501
+ }
2502
+ }, [page, pageCount]);
2503
+ const selectedGroup = participantGroups[page];
2504
+ if (!call)
2505
+ return null;
2506
+ return (jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout__wrapper", ref: setPaginatedGridLayoutWrapperElement, children: [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: participantGroups[page], VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI })), pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { disabled: page === pageCount - 1, icon: "caret-right", onClick: () => setPage((currentPage) => Math.min(pageCount - 1, currentPage + 1)) }))] })] }));
2507
+ };
2508
+
2509
+ const useCalculateHardLimit = (
2510
+ /**
2511
+ * Element that stretches to 100% of the whole layout component
2512
+ */
2513
+ wrapperElement,
2514
+ /**
2515
+ * Element that directly hosts individual `ParticipantView` (or wrapper) elements
2516
+ */
2517
+ hostElement, limit) => {
2518
+ const [calculatedLimit, setCalculatedLimit] = react.useState({
2519
+ vertical: typeof limit === 'number' ? limit : null,
2520
+ horizontal: typeof limit === 'number' ? limit : null,
2521
+ });
2522
+ react.useEffect(() => {
2523
+ if (!hostElement ||
2524
+ !wrapperElement ||
2525
+ typeof limit === 'number' ||
2526
+ typeof limit === 'undefined')
2527
+ return;
2528
+ let childWidth = null;
2529
+ let childHeight = null;
2530
+ const resizeObserver = new ResizeObserver((entries, observer) => {
2531
+ // this part should ideally run as little times as possible
2532
+ // get child measurements and disconnect
2533
+ // does not consider dynamically sized children
2534
+ // this hook is for SpeakerLayout use only, where children in the bar are fixed size
2535
+ if (entries.length > 1) {
2536
+ const child = hostElement.firstChild;
2537
+ if (child) {
2538
+ childHeight = child.clientHeight;
2539
+ childWidth = child.clientWidth;
2540
+ observer.unobserve(hostElement);
2541
+ }
2542
+ }
2543
+ // keep the state at { vertical: 1, horizontal: 1 }
2544
+ // until we get the proper child measurements
2545
+ if (childHeight === null || childWidth === null)
2546
+ return;
2547
+ const vertical = Math.floor(wrapperElement.clientHeight / childHeight);
2548
+ const horizontal = Math.floor(wrapperElement.clientWidth / childWidth);
2549
+ setCalculatedLimit((pv) => {
2550
+ if (pv.vertical !== vertical || pv.horizontal !== horizontal)
2551
+ return { vertical, horizontal };
2552
+ return pv;
2553
+ });
2554
+ });
2555
+ resizeObserver.observe(wrapperElement);
2556
+ resizeObserver.observe(hostElement);
2557
+ return () => {
2558
+ resizeObserver.disconnect();
2559
+ };
2560
+ }, [hostElement, limit, wrapperElement]);
2561
+ return calculatedLimit;
2562
+ };
2563
+
2564
+ const DefaultParticipantViewUIBar = () => (jsxRuntime.jsx(DefaultParticipantViewUI, { menuPlacement: "top-end" }));
2565
+ const DefaultParticipantViewUISpotlight = () => jsxRuntime.jsx(DefaultParticipantViewUI, {});
2566
+ const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, ParticipantViewUISpotlight = DefaultParticipantViewUISpotlight, VideoPlaceholder, participantsBarPosition = 'bottom', participantsBarLimit, }) => {
2567
+ const call = videoReactBindings.useCall();
2568
+ const { useParticipants, useRemoteParticipants } = videoReactBindings.useCallStateHooks();
2569
+ const [participantInSpotlight, ...otherParticipants] = useParticipants();
2570
+ const remoteParticipants = useRemoteParticipants();
2571
+ const [participantsBarWrapperElement, setParticipantsBarWrapperElement] = react.useState(null);
2572
+ const [participantsBarElement, setParticipantsBarElement] = react.useState(null);
2573
+ const [buttonsWrapperElement, setButtonsWrapperElement] = react.useState(null);
2574
+ const isSpeakerScreenSharing = hasScreenShare(participantInSpotlight);
2575
+ const hardLimit = useCalculateHardLimit(buttonsWrapperElement, participantsBarElement, participantsBarLimit);
2576
+ const isVertical = participantsBarPosition === 'left' || participantsBarPosition === 'right';
2577
+ const isHorizontal = participantsBarPosition === 'top' || participantsBarPosition === 'bottom';
2578
+ react.useEffect(() => {
2579
+ if (!participantsBarWrapperElement || !call)
2580
+ return;
2581
+ const cleanup = call.setViewport(participantsBarWrapperElement);
2582
+ return () => cleanup();
2583
+ }, [participantsBarWrapperElement, call]);
2584
+ const isOneOnOneCall = otherParticipants.length === 1;
2585
+ useSpeakerLayoutSortPreset(call, isOneOnOneCall);
2586
+ let participantsWithAppliedLimit = otherParticipants;
2587
+ const hardLimitToApply = isVertical
2588
+ ? hardLimit.vertical
2589
+ : hardLimit.horizontal;
2590
+ if (typeof participantsBarLimit !== 'undefined' &&
2591
+ hardLimitToApply !== null) {
2592
+ participantsWithAppliedLimit = otherParticipants.slice(0,
2593
+ // subtract 1 if speaker is sharing screen as
2594
+ // that one is rendered independently from otherParticipants array
2595
+ hardLimitToApply - (isSpeakerScreenSharing ? 1 : 0));
2596
+ }
2597
+ if (!call)
2598
+ return null;
2599
+ return (jsxRuntime.jsxs("div", { className: "str-video__speaker-layout__wrapper", children: [jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: clsx('str-video__speaker-layout', participantsBarPosition &&
2600
+ `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, trackType: isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack', ParticipantViewUI: ParticipantViewUISpotlight, VideoPlaceholder: VideoPlaceholder })) }), participantsWithAppliedLimit.length > 0 && participantsBarPosition && (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, 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, muteAudio: true }) }, participant.sessionId)))] }) }), isVertical && (jsxRuntime.jsx(VerticalScrollButtons, { scrollWrapper: participantsBarWrapperElement })), isHorizontal && (jsxRuntime.jsx(HorizontalScrollButtons, { scrollWrapper: participantsBarWrapperElement }))] }))] })] }));
2601
+ };
2602
+ const HorizontalScrollButtons = ({ scrollWrapper, }) => {
2603
+ const scrollPosition = useHorizontalScrollPosition(scrollWrapper);
2604
+ const scrollStartClickHandler = () => {
2605
+ scrollWrapper?.scrollBy({ left: -150, behavior: 'smooth' });
2606
+ };
2607
+ const scrollEndClickHandler = () => {
2608
+ scrollWrapper?.scrollBy({ left: 150, behavior: 'smooth' });
2609
+ };
2610
+ 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" }))] }));
2611
+ };
2612
+ const VerticalScrollButtons = ({ scrollWrapper, }) => {
2613
+ const scrollPosition = useVerticalScrollPosition(scrollWrapper);
2614
+ const scrollTopClickHandler = () => {
2615
+ scrollWrapper?.scrollBy({ top: -150, behavior: 'smooth' });
2616
+ };
2617
+ const scrollBottomClickHandler = () => {
2618
+ scrollWrapper?.scrollBy({ top: 150, behavior: 'smooth' });
2619
+ };
2620
+ 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" }))] }));
2621
+ };
2622
+ const hasScreenShare = (p) => !!p?.publishedTracks.includes(videoClient.SfuModels.TrackType.SCREEN_SHARE);
2623
+
2624
+ const [major, minor, patch] = ("0.3.42" ).split('.');
2625
+ videoClient.setSdkInfo({
2626
+ type: videoClient.SfuModels.SdkType.REACT,
2627
+ major,
2628
+ minor,
2629
+ patch,
2630
+ });
2631
+
2632
+ exports.AcceptCallButton = AcceptCallButton;
2633
+ exports.Audio = Audio;
2634
+ exports.Avatar = Avatar;
2635
+ exports.AvatarFallback = AvatarFallback;
2636
+ exports.BaseVideo = BaseVideo;
2637
+ exports.CallControls = CallControls;
2638
+ exports.CallParticipantListing = CallParticipantListing;
2639
+ exports.CallParticipantListingItem = CallParticipantListingItem;
2640
+ exports.CallParticipantsList = CallParticipantsList;
2641
+ exports.CallPreview = CallPreview;
2642
+ exports.CallRecordingList = CallRecordingList;
2643
+ exports.CallRecordingListHeader = CallRecordingListHeader;
2644
+ exports.CallRecordingListItem = CallRecordingListItem;
2645
+ exports.CallStatsButton = CallStatsButton;
2646
+ exports.CancelCallButton = CancelCallButton;
2647
+ exports.CompositeButton = CompositeButton;
2648
+ exports.CopyToClipboardButton = CopyToClipboardButton;
2649
+ exports.CopyToClipboardButtonWithPopup = CopyToClipboardButtonWithPopup;
2650
+ exports.DEVICE_STATE = DEVICE_STATE;
2651
+ exports.DefaultParticipantViewUI = DefaultParticipantViewUI;
2652
+ exports.DefaultReactionsMenu = DefaultReactionsMenu;
2653
+ exports.DefaultScreenShareOverlay = DefaultScreenShareOverlay;
2654
+ exports.DefaultVideoPlaceholder = DefaultVideoPlaceholder;
2655
+ exports.DeviceSelector = DeviceSelector;
2656
+ exports.DeviceSelectorAudioInput = DeviceSelectorAudioInput;
2657
+ exports.DeviceSelectorAudioOutput = DeviceSelectorAudioOutput;
2658
+ exports.DeviceSelectorVideo = DeviceSelectorVideo;
2659
+ exports.DeviceSettings = DeviceSettings;
2660
+ exports.EmptyCallRecordingListing = EmptyCallRecordingListing;
2661
+ exports.GenericMenu = GenericMenu;
2662
+ exports.GenericMenuButtonItem = GenericMenuButtonItem;
2663
+ exports.Icon = Icon;
2664
+ exports.IconButton = IconButton;
2665
+ exports.LivestreamLayout = LivestreamLayout;
2666
+ exports.LoadingCallRecordingListing = LoadingCallRecordingListing;
2667
+ exports.LoadingIndicator = LoadingIndicator;
2668
+ exports.MediaDevicesProvider = MediaDevicesProvider;
2669
+ exports.MenuToggle = MenuToggle;
2670
+ exports.Notification = Notification;
2671
+ exports.PaginatedGridLayout = PaginatedGridLayout;
2672
+ exports.ParticipantActionsContextMenu = ParticipantActionsContextMenu;
2673
+ exports.ParticipantDetails = ParticipantDetails;
2674
+ exports.ParticipantView = ParticipantView;
2675
+ exports.ParticipantsAudio = ParticipantsAudio;
2676
+ exports.PermissionNotification = PermissionNotification;
2677
+ exports.PermissionRequestList = PermissionRequestList;
2678
+ exports.PermissionRequests = PermissionRequests;
2679
+ exports.ReactionsButton = ReactionsButton;
2680
+ exports.RecordCallButton = RecordCallButton;
2681
+ exports.RingingCall = RingingCall;
2682
+ exports.RingingCallControls = RingingCallControls;
2683
+ exports.ScreenShareButton = ScreenShareButton;
2684
+ exports.SearchInput = SearchInput;
2685
+ exports.SearchResults = SearchResults;
2686
+ exports.SpeakerLayout = SpeakerLayout;
2687
+ exports.SpeakingWhileMutedNotification = SpeakingWhileMutedNotification;
2688
+ exports.StreamCall = StreamCall;
2689
+ exports.StreamTheme = StreamTheme;
2690
+ exports.StreamVideo = StreamVideo;
2691
+ exports.TextButton = TextButton;
2692
+ exports.ToggleAudioOutputButton = ToggleAudioOutputButton;
2693
+ exports.ToggleAudioPreviewButton = ToggleAudioPreviewButton;
2694
+ exports.ToggleAudioPublishingButton = ToggleAudioPublishingButton;
2695
+ exports.ToggleVideoPreviewButton = ToggleVideoPreviewButton;
2696
+ exports.ToggleVideoPublishingButton = ToggleVideoPublishingButton;
2697
+ exports.Tooltip = Tooltip;
2698
+ exports.Video = Video$1;
2699
+ exports.VideoPreview = VideoPreview;
2700
+ exports.WithTooltip = WithTooltip;
2701
+ exports.defaultReactions = defaultReactions;
2702
+ exports.translations = translations;
2703
+ exports.useAudioInputDeviceFallback = useAudioInputDeviceFallback;
2704
+ exports.useAudioInputDevices = useAudioInputDevices;
2705
+ exports.useAudioOutputDeviceFallback = useAudioOutputDeviceFallback;
2706
+ exports.useAudioOutputDevices = useAudioOutputDevices;
2707
+ exports.useAudioPublisher = useAudioPublisher;
2708
+ exports.useDeviceFallback = useDeviceFallback;
2709
+ exports.useDevices = useDevices;
2710
+ exports.useHasBrowserPermissions = useHasBrowserPermissions;
2711
+ exports.useHorizontalScrollPosition = useHorizontalScrollPosition;
2712
+ exports.useMediaDevices = useMediaDevices;
2713
+ exports.useOnUnavailableAudioInputDevices = useOnUnavailableAudioInputDevices;
2714
+ exports.useOnUnavailableAudioOutputDevices = useOnUnavailableAudioOutputDevices;
2715
+ exports.useOnUnavailableDevices = useOnUnavailableDevices;
2716
+ exports.useOnUnavailableVideoDevices = useOnUnavailableVideoDevices;
2717
+ exports.useParticipantViewContext = useParticipantViewContext;
2718
+ exports.useToggleAudioMuteState = useToggleAudioMuteState;
2719
+ exports.useToggleVideoMuteState = useToggleVideoMuteState;
2720
+ exports.useTrackElementVisibility = useTrackElementVisibility;
2721
+ exports.useVerticalScrollPosition = useVerticalScrollPosition;
2722
+ exports.useVideoDeviceFallback = useVideoDeviceFallback;
2723
+ exports.useVideoDevices = useVideoDevices;
2724
+ exports.useVideoPublisher = useVideoPublisher;
2725
+ Object.keys(videoClient).forEach(function (k) {
2726
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
2727
+ enumerable: true,
2728
+ get: function () { return videoClient[k]; }
2729
+ });
2730
+ });
2731
+ Object.keys(videoReactBindings).forEach(function (k) {
2732
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
2733
+ enumerable: true,
2734
+ get: function () { return videoReactBindings[k]; }
2735
+ });
2736
+ });
2737
+ //# sourceMappingURL=index.cjs.js.map