@stream-io/video-react-sdk 0.3.40 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/index.cjs.js +2737 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +2633 -0
- package/dist/index.es.js.map +1 -0
- package/dist/src/components/Button/CompositeButton.d.ts +1 -1
- package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +1 -1
- package/dist/src/hooks/useFloatingUIPreset.d.ts +1 -1
- package/index.ts +3 -3
- package/package.json +17 -15
- package/src/components/Permissions/PermissionRequests.tsx +1 -1
- package/dist/index.js +0 -18
- package/dist/index.js.map +0 -1
- package/dist/src/components/Avatar/Avatar.js +0 -24
- package/dist/src/components/Avatar/Avatar.js.map +0 -1
- package/dist/src/components/Avatar/index.js +0 -2
- package/dist/src/components/Avatar/index.js.map +0 -1
- package/dist/src/components/Button/CompositeButton.js +0 -13
- package/dist/src/components/Button/CompositeButton.js.map +0 -1
- package/dist/src/components/Button/CopyToClipboardButton.js +0 -54
- package/dist/src/components/Button/CopyToClipboardButton.js.map +0 -1
- package/dist/src/components/Button/IconButton.js +0 -26
- package/dist/src/components/Button/IconButton.js.map +0 -1
- package/dist/src/components/Button/TextButton.js +0 -17
- package/dist/src/components/Button/TextButton.js.map +0 -1
- package/dist/src/components/Button/index.js +0 -5
- package/dist/src/components/Button/index.js.map +0 -1
- package/dist/src/components/CallControls/AcceptCallButton.js +0 -27
- package/dist/src/components/CallControls/AcceptCallButton.js.map +0 -1
- package/dist/src/components/CallControls/CallControls.js +0 -5
- package/dist/src/components/CallControls/CallControls.js.map +0 -1
- package/dist/src/components/CallControls/CallStatsButton.js +0 -8
- package/dist/src/components/CallControls/CallStatsButton.js.map +0 -1
- package/dist/src/components/CallControls/CancelCallButton.js +0 -27
- package/dist/src/components/CallControls/CancelCallButton.js.map +0 -1
- package/dist/src/components/CallControls/ReactionsButton.js +0 -45
- package/dist/src/components/CallControls/ReactionsButton.js.map +0 -1
- package/dist/src/components/CallControls/RecordCallButton.js +0 -22
- package/dist/src/components/CallControls/RecordCallButton.js.map +0 -1
- package/dist/src/components/CallControls/ScreenShareButton.js +0 -15
- package/dist/src/components/CallControls/ScreenShareButton.js.map +0 -1
- package/dist/src/components/CallControls/ToggleAudioButton.js +0 -24
- package/dist/src/components/CallControls/ToggleAudioButton.js.map +0 -1
- package/dist/src/components/CallControls/ToggleAudioOutputButton.js +0 -10
- package/dist/src/components/CallControls/ToggleAudioOutputButton.js.map +0 -1
- package/dist/src/components/CallControls/ToggleVideoButton.js +0 -24
- package/dist/src/components/CallControls/ToggleVideoButton.js.map +0 -1
- package/dist/src/components/CallControls/index.js +0 -11
- package/dist/src/components/CallControls/index.js.map +0 -1
- package/dist/src/components/CallParticipantsList/BlockedUserListing.js +0 -18
- package/dist/src/components/CallParticipantsList/BlockedUserListing.js.map +0 -1
- package/dist/src/components/CallParticipantsList/CallParticipantListHeader.js +0 -10
- package/dist/src/components/CallParticipantsList/CallParticipantListHeader.js.map +0 -1
- package/dist/src/components/CallParticipantsList/CallParticipantListing.js +0 -4
- package/dist/src/components/CallParticipantsList/CallParticipantListing.js.map +0 -1
- package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js +0 -128
- package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js.map +0 -1
- package/dist/src/components/CallParticipantsList/CallParticipantsList.js +0 -83
- package/dist/src/components/CallParticipantsList/CallParticipantsList.js.map +0 -1
- package/dist/src/components/CallParticipantsList/EmptyParticipantSearchList.js +0 -7
- package/dist/src/components/CallParticipantsList/EmptyParticipantSearchList.js.map +0 -1
- package/dist/src/components/CallParticipantsList/index.js +0 -4
- package/dist/src/components/CallParticipantsList/index.js.map +0 -1
- package/dist/src/components/CallPreview/CallPreview.js +0 -21
- package/dist/src/components/CallPreview/CallPreview.js.map +0 -1
- package/dist/src/components/CallPreview/index.js +0 -2
- package/dist/src/components/CallPreview/index.js.map +0 -1
- package/dist/src/components/CallRecordingList/CallRecordingList.js +0 -9
- package/dist/src/components/CallRecordingList/CallRecordingList.js.map +0 -1
- package/dist/src/components/CallRecordingList/CallRecordingListHeader.js +0 -6
- package/dist/src/components/CallRecordingList/CallRecordingListHeader.js.map +0 -1
- package/dist/src/components/CallRecordingList/CallRecordingListItem.js +0 -11
- package/dist/src/components/CallRecordingList/CallRecordingListItem.js.map +0 -1
- package/dist/src/components/CallRecordingList/EmptyCallRecordingListing.js +0 -5
- package/dist/src/components/CallRecordingList/EmptyCallRecordingListing.js.map +0 -1
- package/dist/src/components/CallRecordingList/LoadingCallRecordingListing.js +0 -7
- package/dist/src/components/CallRecordingList/LoadingCallRecordingListing.js.map +0 -1
- package/dist/src/components/CallRecordingList/index.js +0 -6
- package/dist/src/components/CallRecordingList/index.js.map +0 -1
- package/dist/src/components/CallStats/CallStats.js +0 -70
- package/dist/src/components/CallStats/CallStats.js.map +0 -1
- package/dist/src/components/CallStats/CallStatsLatencyChart.js +0 -39
- package/dist/src/components/CallStats/CallStatsLatencyChart.js.map +0 -1
- package/dist/src/components/CallStats/index.js +0 -3
- package/dist/src/components/CallStats/index.js.map +0 -1
- package/dist/src/components/Debug/DebugParticipantPublishQuality.js +0 -46
- package/dist/src/components/Debug/DebugParticipantPublishQuality.js.map +0 -1
- package/dist/src/components/Debug/DebugStatsView.js +0 -66
- package/dist/src/components/Debug/DebugStatsView.js.map +0 -1
- package/dist/src/components/Debug/useIsDebugMode.js +0 -18
- package/dist/src/components/Debug/useIsDebugMode.js.map +0 -1
- package/dist/src/components/DeviceSettings/DeviceSelector.js +0 -26
- package/dist/src/components/DeviceSettings/DeviceSelector.js.map +0 -1
- package/dist/src/components/DeviceSettings/DeviceSelectorAudio.js +0 -20
- package/dist/src/components/DeviceSettings/DeviceSelectorAudio.js.map +0 -1
- package/dist/src/components/DeviceSettings/DeviceSelectorVideo.js +0 -11
- package/dist/src/components/DeviceSettings/DeviceSelectorVideo.js.map +0 -1
- package/dist/src/components/DeviceSettings/DeviceSettings.js +0 -15
- package/dist/src/components/DeviceSettings/DeviceSettings.js.map +0 -1
- package/dist/src/components/DeviceSettings/index.js +0 -5
- package/dist/src/components/DeviceSettings/index.js.map +0 -1
- package/dist/src/components/Icon/Icon.js +0 -4
- package/dist/src/components/Icon/Icon.js.map +0 -1
- package/dist/src/components/Icon/index.js +0 -2
- package/dist/src/components/Icon/index.js.map +0 -1
- package/dist/src/components/LoadingIndicator/LoadingIndicator.js +0 -6
- package/dist/src/components/LoadingIndicator/LoadingIndicator.js.map +0 -1
- package/dist/src/components/LoadingIndicator/index.js +0 -2
- package/dist/src/components/LoadingIndicator/index.js.map +0 -1
- package/dist/src/components/Menu/GenericMenu.js +0 -20
- package/dist/src/components/Menu/GenericMenu.js.map +0 -1
- package/dist/src/components/Menu/MenuToggle.js +0 -40
- package/dist/src/components/Menu/MenuToggle.js.map +0 -1
- package/dist/src/components/Menu/index.js +0 -3
- package/dist/src/components/Menu/index.js.map +0 -1
- package/dist/src/components/Notification/Notification.js +0 -25
- package/dist/src/components/Notification/Notification.js.map +0 -1
- package/dist/src/components/Notification/PermissionNotification.js +0 -26
- package/dist/src/components/Notification/PermissionNotification.js.map +0 -1
- package/dist/src/components/Notification/SpeakingWhileMutedNotification.js +0 -50
- package/dist/src/components/Notification/SpeakingWhileMutedNotification.js.map +0 -1
- package/dist/src/components/Notification/index.js +0 -4
- package/dist/src/components/Notification/index.js.map +0 -1
- package/dist/src/components/Permissions/PermissionRequests.js +0 -122
- package/dist/src/components/Permissions/PermissionRequests.js.map +0 -1
- package/dist/src/components/Permissions/index.js +0 -2
- package/dist/src/components/Permissions/index.js.map +0 -1
- package/dist/src/components/Reaction/Reaction.js +0 -29
- package/dist/src/components/Reaction/Reaction.js.map +0 -1
- package/dist/src/components/Reaction/index.js +0 -2
- package/dist/src/components/Reaction/index.js.map +0 -1
- package/dist/src/components/RingingCall/RingingCall.js +0 -45
- package/dist/src/components/RingingCall/RingingCall.js.map +0 -1
- package/dist/src/components/RingingCall/RingingCallControls.js +0 -14
- package/dist/src/components/RingingCall/RingingCallControls.js.map +0 -1
- package/dist/src/components/RingingCall/index.js +0 -3
- package/dist/src/components/RingingCall/index.js.map +0 -1
- package/dist/src/components/Search/SearchInput.js +0 -34
- package/dist/src/components/Search/SearchInput.js.map +0 -1
- package/dist/src/components/Search/SearchResults.js +0 -12
- package/dist/src/components/Search/SearchResults.js.map +0 -1
- package/dist/src/components/Search/hooks/index.js +0 -2
- package/dist/src/components/Search/hooks/index.js.map +0 -1
- package/dist/src/components/Search/hooks/useSearch.js +0 -39
- package/dist/src/components/Search/hooks/useSearch.js.map +0 -1
- package/dist/src/components/Search/index.js +0 -3
- package/dist/src/components/Search/index.js.map +0 -1
- package/dist/src/components/StreamTheme/StreamTheme.js +0 -18
- package/dist/src/components/StreamTheme/StreamTheme.js.map +0 -1
- package/dist/src/components/StreamTheme/index.js +0 -2
- package/dist/src/components/StreamTheme/index.js.map +0 -1
- package/dist/src/components/Tooltip/Tooltip.js +0 -22
- package/dist/src/components/Tooltip/Tooltip.js.map +0 -1
- package/dist/src/components/Tooltip/WithTooltip.js +0 -23
- package/dist/src/components/Tooltip/WithTooltip.js.map +0 -1
- package/dist/src/components/Tooltip/hooks/index.js +0 -2
- package/dist/src/components/Tooltip/hooks/index.js.map +0 -1
- package/dist/src/components/Tooltip/hooks/useEnterLeaveHandlers.js +0 -14
- package/dist/src/components/Tooltip/hooks/useEnterLeaveHandlers.js.map +0 -1
- package/dist/src/components/Tooltip/index.js +0 -3
- package/dist/src/components/Tooltip/index.js.map +0 -1
- package/dist/src/components/Video/VideoPreview.js +0 -75
- package/dist/src/components/Video/VideoPreview.js.map +0 -1
- package/dist/src/components/Video/index.js +0 -5
- package/dist/src/components/Video/index.js.map +0 -1
- package/dist/src/components/index.js +0 -18
- package/dist/src/components/index.js.map +0 -1
- package/dist/src/core/components/Audio/Audio.js +0 -30
- package/dist/src/core/components/Audio/Audio.js.map +0 -1
- package/dist/src/core/components/Audio/ParticipantsAudio.js +0 -21
- package/dist/src/core/components/Audio/ParticipantsAudio.js.map +0 -1
- package/dist/src/core/components/Audio/index.js +0 -3
- package/dist/src/core/components/Audio/index.js.map +0 -1
- package/dist/src/core/components/CallLayout/LivestreamLayout.js +0 -89
- package/dist/src/core/components/CallLayout/LivestreamLayout.js.map +0 -1
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.js +0 -47
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.js.map +0 -1
- package/dist/src/core/components/CallLayout/SpeakerLayout.js +0 -71
- package/dist/src/core/components/CallLayout/SpeakerLayout.js.map +0 -1
- package/dist/src/core/components/CallLayout/hooks.js +0 -41
- package/dist/src/core/components/CallLayout/hooks.js.map +0 -1
- package/dist/src/core/components/CallLayout/index.js +0 -4
- package/dist/src/core/components/CallLayout/index.js.map +0 -1
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js +0 -48
- package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js.map +0 -1
- package/dist/src/core/components/ParticipantView/ParticipantView.js +0 -54
- package/dist/src/core/components/ParticipantView/ParticipantView.js.map +0 -1
- package/dist/src/core/components/ParticipantView/index.js +0 -3
- package/dist/src/core/components/ParticipantView/index.js.map +0 -1
- package/dist/src/core/components/StreamCall/StreamCall.js +0 -7
- package/dist/src/core/components/StreamCall/StreamCall.js.map +0 -1
- package/dist/src/core/components/StreamCall/index.js +0 -2
- package/dist/src/core/components/StreamCall/index.js.map +0 -1
- package/dist/src/core/components/StreamVideo/StreamVideo.js +0 -7
- package/dist/src/core/components/StreamVideo/StreamVideo.js.map +0 -1
- package/dist/src/core/components/StreamVideo/index.js +0 -2
- package/dist/src/core/components/StreamVideo/index.js.map +0 -1
- package/dist/src/core/components/Video/BaseVideo.js +0 -48
- package/dist/src/core/components/Video/BaseVideo.js.map +0 -1
- package/dist/src/core/components/Video/DefaultVideoPlaceholder.js +0 -9
- package/dist/src/core/components/Video/DefaultVideoPlaceholder.js.map +0 -1
- package/dist/src/core/components/Video/Video.js +0 -82
- package/dist/src/core/components/Video/Video.js.map +0 -1
- package/dist/src/core/components/Video/index.js +0 -3
- package/dist/src/core/components/Video/index.js.map +0 -1
- package/dist/src/core/components/index.js +0 -7
- package/dist/src/core/components/index.js.map +0 -1
- package/dist/src/core/contexts/MediaDevicesContext.js +0 -178
- package/dist/src/core/contexts/MediaDevicesContext.js.map +0 -1
- package/dist/src/core/contexts/index.js +0 -2
- package/dist/src/core/contexts/index.js.map +0 -1
- package/dist/src/core/hooks/index.js +0 -5
- package/dist/src/core/hooks/index.js.map +0 -1
- package/dist/src/core/hooks/useAudioPublisher.js +0 -114
- package/dist/src/core/hooks/useAudioPublisher.js.map +0 -1
- package/dist/src/core/hooks/useCalculateHardLimit.js +0 -56
- package/dist/src/core/hooks/useCalculateHardLimit.js.map +0 -1
- package/dist/src/core/hooks/useDevices.js +0 -172
- package/dist/src/core/hooks/useDevices.js.map +0 -1
- package/dist/src/core/hooks/useTrackElementVisibility.js +0 -15
- package/dist/src/core/hooks/useTrackElementVisibility.js.map +0 -1
- package/dist/src/core/hooks/useVideoPublisher.js +0 -139
- package/dist/src/core/hooks/useVideoPublisher.js.map +0 -1
- package/dist/src/core/index.js +0 -4
- package/dist/src/core/index.js.map +0 -1
- package/dist/src/hooks/index.js +0 -8
- package/dist/src/hooks/index.js.map +0 -1
- package/dist/src/hooks/useFloatingUIPreset.js +0 -30
- package/dist/src/hooks/useFloatingUIPreset.js.map +0 -1
- package/dist/src/hooks/useRequestPermission.js +0 -46
- package/dist/src/hooks/useRequestPermission.js.map +0 -1
- package/dist/src/hooks/useScrollPosition.js +0 -63
- package/dist/src/hooks/useScrollPosition.js.map +0 -1
- package/dist/src/hooks/useToggleAudioMuteState.js +0 -34
- package/dist/src/hooks/useToggleAudioMuteState.js.map +0 -1
- package/dist/src/hooks/useToggleCallRecording.js +0 -44
- package/dist/src/hooks/useToggleCallRecording.js.map +0 -1
- package/dist/src/hooks/useToggleScreenShare.js +0 -38
- package/dist/src/hooks/useToggleScreenShare.js.map +0 -1
- package/dist/src/hooks/useToggleVideoMuteState.js +0 -34
- package/dist/src/hooks/useToggleVideoMuteState.js.map +0 -1
- package/dist/src/translations/en.json +0 -73
- package/dist/src/translations/index.js +0 -3
- package/dist/src/translations/index.js.map +0 -1
- package/dist/src/types/components.js +0 -2
- package/dist/src/types/components.js.map +0 -1
- package/dist/src/types/index.js +0 -2
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/utilities/applyElementToRef.js +0 -8
- package/dist/src/utilities/applyElementToRef.js.map +0 -1
- package/dist/src/utilities/chunk.js +0 -5
- package/dist/src/utilities/chunk.js.map +0 -1
- package/dist/src/utilities/index.js +0 -4
- package/dist/src/utilities/index.js.map +0 -1
- package/dist/src/utilities/isComponentType.js +0 -7
- package/dist/src/utilities/isComponentType.js.map +0 -1
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -2
- 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
|