UNPKG

communication-react-19

Version:

React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)

332 lines 22.7 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { concatStyleSets, mergeStyles, Stack } from '@fluentui/react'; import React, { useCallback, useMemo, useRef } from 'react'; import { Announcer } from './Announcer'; import { useEffect } from 'react'; import { useLocale } from '../localization'; import { useTheme } from '../theming'; import { _RemoteVideoTile } from './RemoteVideoTile'; import { isNarrowWidth, _useContainerHeight, _useContainerWidth } from './utils/responsive'; import { LocalScreenShare } from './VideoGallery/LocalScreenShare'; import { RemoteScreenShare } from './VideoGallery/RemoteScreenShare'; import { _isIdentityMicrosoftTeamsUser } from "../../../acs-ui-common/src"; import { _LocalVideoTile } from './LocalVideoTile'; import { DefaultLayout } from './VideoGallery/DefaultLayout'; import { FloatingLocalVideoLayout } from './VideoGallery/FloatingLocalVideoLayout'; import { useIdentifiers } from '../identifiers'; import { localVideoTileContainerStyles, videoGalleryOuterDivStyle } from './styles/VideoGallery.styles'; import { floatingLocalVideoTileStyle } from './VideoGallery/styles/FloatingLocalVideo.styles'; import { useId } from '@fluentui/react-hooks'; import { SpeakerVideoLayout } from './VideoGallery/SpeakerVideoLayout'; import { FocusedContentLayout } from './VideoGallery/FocusContentLayout'; /* @conditional-compile-remove(large-gallery) */ import { LargeGalleryLayout } from './VideoGallery/LargeGalleryLayout'; import { TogetherModeLayout } from './VideoGallery/TogetherModeLayout'; import { TogetherModeStream } from './VideoGallery/TogetherModeStream'; /** * @private * Currently the Calling JS SDK supports up to 4 remote video streams */ export const DEFAULT_MAX_REMOTE_VIDEO_STREAMS = 4; /** * @private * Styles to disable the selectivity of a text in video gallery */ export const unselectable = { '-webkit-user-select': 'none', '-webkit-touch-callout': 'none', '-moz-user-select': 'none', '-ms-user-select': 'none', 'user-select': 'none' }; /** * @private * Set aside only 6 dominant speakers for remaining audio participants */ export const MAX_AUDIO_DOMINANT_SPEAKERS = 6; /** * @private * Default remote video tile menu options */ export const DEFAULT_REMOTE_VIDEO_TILE_MENU_OPTIONS = { kind: 'contextual' }; /** * @private * Maximum number of remote video tiles that can be pinned */ export const MAX_PINNED_REMOTE_VIDEO_TILES = 4; /** * VideoGallery represents a layout of video tiles for a specific call. * It displays a {@link VideoTile} for the local user as well as for each remote participant who has joined the call. * * @public */ export const VideoGallery = (props) => { var _a, _b, _c, _d; const { localParticipant, remoteParticipants = [], localVideoViewOptions, remoteVideoViewOptions, dominantSpeakers, onRenderLocalVideoTile, onRenderRemoteVideoTile, onCreateLocalStreamView, onDisposeLocalStreamView, onCreateRemoteStreamView, onDisposeRemoteScreenShareStreamView, onDisposeLocalScreenShareStreamView, onDisposeRemoteVideoStreamView, styles, layout, onRenderAvatar, showMuteIndicator, maxRemoteVideoStreams = DEFAULT_MAX_REMOTE_VIDEO_STREAMS, showCameraSwitcherInLocalPreview, localVideoCameraCycleButtonProps, onPinParticipant: onPinParticipantHandler, onUnpinParticipant: onUnpinParticipantHandler, remoteVideoTileMenu = DEFAULT_REMOTE_VIDEO_TILE_MENU_OPTIONS, overflowGalleryPosition = 'horizontalBottom', localVideoTileSize = 'followDeviceOrientation', spotlightedParticipants, onStartLocalSpotlight, onStartRemoteSpotlight, onStopLocalSpotlight, onStopRemoteSpotlight, maxParticipantsToSpotlight, reactionResources, videoTilesOptions, onMuteParticipant, startTogetherModeEnabled, isTogetherModeActive, onCreateTogetherModeStreamView, onStartTogetherMode, onSetTogetherModeSceneSize, togetherModeStreams, togetherModeSeatingCoordinates, onDisposeTogetherModeStreamView, onForbidAudio, onPermitAudio, onForbidVideo, onPermitVideo, localScreenShareView } = props; const ids = useIdentifiers(); const theme = useTheme(); const localeStrings = useLocale().strings.videoGallery; const strings = useMemo(() => (Object.assign(Object.assign({}, localeStrings), props.strings)), [localeStrings, props.strings]); const drawerMenuHostIdFromProp = remoteVideoTileMenu && remoteVideoTileMenu.kind === 'drawer' ? remoteVideoTileMenu.hostId : undefined; const drawerMenuHostId = useId('drawerMenuHost', drawerMenuHostIdFromProp); const localTileNotInGrid = (layout === 'floatingLocalVideo' || layout === 'speaker') && remoteParticipants.length > 0; const containerRef = useRef(null); const containerWidth = _useContainerWidth(containerRef); const containerHeight = _useContainerHeight(containerRef); const isNarrow = containerWidth ? isNarrowWidth(containerWidth) : false; const [pinnedParticipantsState, setPinnedParticipantsState] = React.useState([]); const [selectedScalingModeState, setselectedScalingModeState] = React.useState({}); const onUpdateScalingMode = useCallback((remoteUserId, scalingMode) => { setselectedScalingModeState((current) => (Object.assign(Object.assign({}, current), { [remoteUserId]: { scalingMode, isMirrored: remoteVideoViewOptions === null || remoteVideoViewOptions === void 0 ? void 0 : remoteVideoViewOptions.isMirrored } }))); }, [remoteVideoViewOptions === null || remoteVideoViewOptions === void 0 ? void 0 : remoteVideoViewOptions.isMirrored]); useEffect(() => { var _a; (_a = props.pinnedParticipants) === null || _a === void 0 ? void 0 : _a.forEach((pinParticipant) => { var _a; if (!((_a = props.remoteParticipants) === null || _a === void 0 ? void 0 : _a.find((t) => t.userId === pinParticipant))) { // warning will be logged in the console when invalid participant id is passed in pinned participants console.warn('Invalid pinned participant UserId :' + pinParticipant); } }); }, [props.pinnedParticipants, props.remoteParticipants]); // Use pinnedParticipants from props but if it is not defined use the maintained state of pinned participants const pinnedParticipants = useMemo(() => { var _a; return (_a = props.pinnedParticipants) !== null && _a !== void 0 ? _a : pinnedParticipantsState.filter((pinnedParticipantId) => remoteParticipants.find((remoteParticipant) => remoteParticipant.userId === pinnedParticipantId)); }, [props.pinnedParticipants, pinnedParticipantsState, remoteParticipants]); const showLocalVideoTileLabel = !((localTileNotInGrid && isNarrow && localVideoTileSize !== '16:9') || localVideoTileSize === '9:16') || layout === 'default'; /** * Utility function for memoized rendering of LocalParticipant. */ const localVideoTile = useMemo(() => { var _a, _b; if (localVideoTileSize === 'hidden') { return undefined; } if (onRenderLocalVideoTile) { return onRenderLocalVideoTile(localParticipant); } const isSpotlighted = !!localParticipant.spotlight; const localVideoTileStyles = concatStyleSets(localTileNotInGrid ? floatingLocalVideoTileStyle : {}, { root: { borderRadius: theme.effects.roundedCorner4 } }, styles === null || styles === void 0 ? void 0 : styles.localVideo); const initialsName = !localParticipant.displayName ? '' : localParticipant.displayName; const showDisplayNameTrampoline = () => { return layout === 'default' ? strings.localVideoLabel : isNarrow && localVideoTileSize !== '16:9' ? '' : strings.localVideoLabel; }; return (React.createElement(Stack, { styles: localVideoTileContainerStyles, key: "local-video-tile-key" }, React.createElement(_LocalVideoTile, { alwaysShowLabelBackground: videoTilesOptions === null || videoTilesOptions === void 0 ? void 0 : videoTilesOptions.alwaysShowLabelBackground, userId: localParticipant.userId, onCreateLocalStreamView: onCreateLocalStreamView, onDisposeLocalStreamView: onDisposeLocalStreamView, isAvailable: (_a = localParticipant === null || localParticipant === void 0 ? void 0 : localParticipant.videoStream) === null || _a === void 0 ? void 0 : _a.isAvailable, isMuted: localParticipant.isMuted, renderElement: (_b = localParticipant === null || localParticipant === void 0 ? void 0 : localParticipant.videoStream) === null || _b === void 0 ? void 0 : _b.renderElement, displayName: showDisplayNameTrampoline(), initialsName: initialsName, localVideoViewOptions: localVideoViewOptions, onRenderAvatar: onRenderAvatar, showLabel: showLocalVideoTileLabel, showMuteIndicator: showMuteIndicator, showCameraSwitcherInLocalPreview: showCameraSwitcherInLocalPreview, localVideoCameraCycleButtonProps: localVideoCameraCycleButtonProps, localVideoCameraSwitcherLabel: strings.localVideoCameraSwitcherLabel, localVideoSelectedDescription: strings.localVideoSelectedDescription, styles: localVideoTileStyles, raisedHand: localParticipant.raisedHand, reaction: localParticipant.reaction, spotlightedParticipantUserIds: spotlightedParticipants, isSpotlighted: isSpotlighted, onStartSpotlight: onStartLocalSpotlight, onStopSpotlight: onStopLocalSpotlight, maxParticipantsToSpotlight: maxParticipantsToSpotlight, menuKind: remoteVideoTileMenu ? (remoteVideoTileMenu.kind === 'drawer' ? 'drawer' : 'contextual') : undefined, drawerMenuHostId: drawerMenuHostId, strings: strings, reactionResources: reactionResources, participantsCount: remoteParticipants.length + 1, isScreenSharingOn: localParticipant.isScreenSharingOn, mediaAccess: localParticipant.mediaAccess }))); }, [ isNarrow, localParticipant, localVideoCameraCycleButtonProps, localVideoViewOptions, onCreateLocalStreamView, onDisposeLocalStreamView, onRenderAvatar, onRenderLocalVideoTile, localTileNotInGrid, showCameraSwitcherInLocalPreview, showMuteIndicator, styles === null || styles === void 0 ? void 0 : styles.localVideo, theme.effects.roundedCorner4, localVideoTileSize, layout, showLocalVideoTileLabel, spotlightedParticipants, onStartLocalSpotlight, onStopLocalSpotlight, maxParticipantsToSpotlight, remoteVideoTileMenu, strings, drawerMenuHostId, reactionResources, videoTilesOptions, remoteParticipants.length ]); const onPinParticipant = useCallback((userId) => { if (pinnedParticipants.length >= MAX_PINNED_REMOTE_VIDEO_TILES) { return; } if (!pinnedParticipants.includes(userId)) { setPinnedParticipantsState(pinnedParticipants.concat(userId)); } onPinParticipantHandler === null || onPinParticipantHandler === void 0 ? void 0 : onPinParticipantHandler(userId); }, [pinnedParticipants, setPinnedParticipantsState, onPinParticipantHandler]); const onUnpinParticipant = useCallback((userId) => { setPinnedParticipantsState(pinnedParticipantsState.filter((p) => p !== userId)); onUnpinParticipantHandler === null || onUnpinParticipantHandler === void 0 ? void 0 : onUnpinParticipantHandler(userId); }, [pinnedParticipantsState, setPinnedParticipantsState, onUnpinParticipantHandler]); const [announcementString, setAnnouncementString] = React.useState(''); /** * sets the announcement string for VideoGallery actions so that the screenreader will trigger */ const toggleAnnouncerString = useCallback((announcement) => { setAnnouncementString(announcement); /** * Clears the announcer string after VideoGallery action allowing it to be re-announced. */ setTimeout(() => { setAnnouncementString(''); }, 3000); }, [setAnnouncementString]); const defaultOnRenderVideoTile = useCallback((participant, isVideoParticipant) => { const remoteVideoStream = participant.videoStream; const selectedScalingMode = remoteVideoStream ? selectedScalingModeState[participant.userId] : undefined; let isPinned = pinnedParticipants === null || pinnedParticipants === void 0 ? void 0 : pinnedParticipants.includes(participant.userId); const isSpotlighted = !!participant.spotlight; isPinned = isSpotlighted ? false : isPinned; const createViewOptions = () => { var _a, _b; if (selectedScalingMode) { return selectedScalingMode; } return (remoteVideoStream === null || remoteVideoStream === void 0 ? void 0 : remoteVideoStream.streamSize) && ((_a = remoteVideoStream.streamSize) === null || _a === void 0 ? void 0 : _a.height) > ((_b = remoteVideoStream.streamSize) === null || _b === void 0 ? void 0 : _b.width) ? { scalingMode: 'Fit', isMirrored: remoteVideoViewOptions === null || remoteVideoViewOptions === void 0 ? void 0 : remoteVideoViewOptions.isMirrored } : remoteVideoViewOptions; }; return (React.createElement(_RemoteVideoTile, { alwaysShowLabelBackground: videoTilesOptions === null || videoTilesOptions === void 0 ? void 0 : videoTilesOptions.alwaysShowLabelBackground, streamId: remoteVideoStream === null || remoteVideoStream === void 0 ? void 0 : remoteVideoStream.id, key: participant.userId, userId: participant.userId, remoteParticipant: participant, onCreateRemoteStreamView: isVideoParticipant ? onCreateRemoteStreamView : undefined, onDisposeRemoteStreamView: isVideoParticipant ? onDisposeRemoteVideoStreamView : undefined, isAvailable: isVideoParticipant ? remoteVideoStream === null || remoteVideoStream === void 0 ? void 0 : remoteVideoStream.isAvailable : false, isReceiving: isVideoParticipant ? remoteVideoStream === null || remoteVideoStream === void 0 ? void 0 : remoteVideoStream.isReceiving : false, renderElement: isVideoParticipant ? remoteVideoStream === null || remoteVideoStream === void 0 ? void 0 : remoteVideoStream.renderElement : undefined, remoteVideoViewOptions: createViewOptions(), onRenderAvatar: onRenderAvatar, showMuteIndicator: showMuteIndicator, strings: strings, participantState: participant.state, menuKind: participant.userId === localParticipant.userId ? undefined : remoteVideoTileMenu ? remoteVideoTileMenu.kind === 'drawer' ? 'drawer' : 'contextual' : undefined, drawerMenuHostId: drawerMenuHostId, onPinParticipant: onPinParticipant, onUnpinParticipant: onUnpinParticipant, onUpdateScalingMode: onUpdateScalingMode, isPinned: isPinned, disablePinMenuItem: pinnedParticipants.length >= MAX_PINNED_REMOTE_VIDEO_TILES, toggleAnnouncerString: toggleAnnouncerString, spotlightedParticipantUserIds: spotlightedParticipants, isSpotlighted: isSpotlighted, onStartSpotlight: onStartRemoteSpotlight, onStopSpotlight: onStopRemoteSpotlight, maxParticipantsToSpotlight: maxParticipantsToSpotlight, reactionResources: reactionResources, onMuteParticipant: onMuteParticipant, onForbidAudio: onForbidAudio, onPermitAudio: onPermitAudio, onForbidVideo: onForbidVideo, onPermitVideo: onPermitVideo })); }, [ selectedScalingModeState, pinnedParticipants, videoTilesOptions === null || videoTilesOptions === void 0 ? void 0 : videoTilesOptions.alwaysShowLabelBackground, onCreateRemoteStreamView, onDisposeRemoteVideoStreamView, onRenderAvatar, showMuteIndicator, strings, localParticipant.userId, remoteVideoTileMenu, drawerMenuHostId, onPinParticipant, onUnpinParticipant, onUpdateScalingMode, toggleAnnouncerString, spotlightedParticipants, onStartRemoteSpotlight, onStopRemoteSpotlight, maxParticipantsToSpotlight, reactionResources, onMuteParticipant, onForbidAudio, onPermitAudio, onForbidVideo, onPermitVideo, remoteVideoViewOptions ]); const screenShareParticipant = remoteParticipants.find((participant) => { var _a; return (_a = participant.screenShareStream) === null || _a === void 0 ? void 0 : _a.isAvailable; }); const localScreenShareStreamComponent = (React.createElement(LocalScreenShare, { localParticipant: localParticipant, renderElement: (_a = localParticipant.screenShareStream) === null || _a === void 0 ? void 0 : _a.renderElement, isAvailable: (_b = localParticipant.screenShareStream) === null || _b === void 0 ? void 0 : _b.isAvailable, onCreateLocalStreamView: onCreateLocalStreamView, onDisposeLocalScreenShareStreamView: onDisposeLocalScreenShareStreamView, localScreenShareView: localScreenShareView })); const remoteScreenShareComponent = screenShareParticipant && (React.createElement(RemoteScreenShare, Object.assign({}, screenShareParticipant, { renderElement: (_c = screenShareParticipant.screenShareStream) === null || _c === void 0 ? void 0 : _c.renderElement, onCreateRemoteStreamView: onCreateRemoteStreamView, onDisposeRemoteStreamView: onDisposeRemoteScreenShareStreamView, isReceiving: (_d = screenShareParticipant.screenShareStream) === null || _d === void 0 ? void 0 : _d.isReceiving, participantVideoScalingMode: selectedScalingModeState[screenShareParticipant.userId], localParticipant: localParticipant, remoteParticipants: remoteParticipants, reactionResources: reactionResources }))); const screenShareComponent = remoteScreenShareComponent ? remoteScreenShareComponent : localParticipant.isScreenSharingOn ? localScreenShareStreamComponent : undefined; // Current implementation of capabilities is only based on user role. // This logic checks for the user role and if the user is a Teams user. const canSwitchToTogetherModeLayout = isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); const togetherModeStreamComponent = useMemo(() => // Avoids unnecessary rendering of TogetherModeStream component when it is not needed !screenShareComponent && canSwitchToTogetherModeLayout && layout === 'togetherMode' ? (React.createElement(TogetherModeStream, { startTogetherModeEnabled: startTogetherModeEnabled, isTogetherModeActive: isTogetherModeActive, onCreateTogetherModeStreamView: onCreateTogetherModeStreamView, onStartTogetherMode: onStartTogetherMode, onDisposeTogetherModeStreamView: onDisposeTogetherModeStreamView, onSetTogetherModeSceneSize: onSetTogetherModeSceneSize, togetherModeStreams: togetherModeStreams, seatingCoordinates: togetherModeSeatingCoordinates, localParticipant: localParticipant, remoteParticipants: remoteParticipants, reactionResources: reactionResources, containerWidth: containerWidth, containerHeight: containerHeight })) : undefined, [ layout, screenShareComponent, canSwitchToTogetherModeLayout, startTogetherModeEnabled, isTogetherModeActive, onCreateTogetherModeStreamView, onStartTogetherMode, onDisposeTogetherModeStreamView, onSetTogetherModeSceneSize, togetherModeStreams, togetherModeSeatingCoordinates, localParticipant, remoteParticipants, reactionResources, containerWidth, containerHeight ]); const layoutProps = useMemo(() => ({ remoteParticipants, localParticipant, screenShareComponent, showCameraSwitcherInLocalPreview, maxRemoteVideoStreams, dominantSpeakers, styles, onRenderRemoteParticipant: onRenderRemoteVideoTile !== null && onRenderRemoteVideoTile !== void 0 ? onRenderRemoteVideoTile : defaultOnRenderVideoTile, localVideoComponent: localVideoTile, parentWidth: containerWidth, parentHeight: containerHeight, pinnedParticipantUserIds: pinnedParticipants, overflowGalleryPosition, localVideoTileSize, spotlightedParticipantUserIds: spotlightedParticipants }), [ remoteParticipants, localParticipant, screenShareComponent, showCameraSwitcherInLocalPreview, maxRemoteVideoStreams, dominantSpeakers, styles, localVideoTile, containerWidth, containerHeight, onRenderRemoteVideoTile, defaultOnRenderVideoTile, pinnedParticipants, overflowGalleryPosition, localVideoTileSize, spotlightedParticipants ]); const videoGalleryLayout = useMemo(() => { if (screenShareParticipant && layout === 'focusedContent') { return React.createElement(FocusedContentLayout, Object.assign({}, layoutProps)); } if (layout === 'floatingLocalVideo') { return React.createElement(FloatingLocalVideoLayout, Object.assign({}, layoutProps)); } if (layout === 'speaker') { return React.createElement(SpeakerVideoLayout, Object.assign({}, layoutProps)); } /* @conditional-compile-remove(large-gallery) */ if (layout === 'largeGallery') { return React.createElement(LargeGalleryLayout, Object.assign({}, layoutProps)); } // Teams users can switch to Together mode layout only if they have the capability, // while ACS users can do so only if Together mode is enabled. if (togetherModeStreamComponent && layout === 'togetherMode') { return React.createElement(TogetherModeLayout, { togetherModeStreamComponent: togetherModeStreamComponent }); } return React.createElement(DefaultLayout, Object.assign({}, layoutProps)); }, [layout, layoutProps, screenShareParticipant, togetherModeStreamComponent]); return (React.createElement("div", { // We don't assign an drawer menu host id to the VideoGallery when a drawerMenuHostId is assigned from props id: drawerMenuHostIdFromProp ? undefined : drawerMenuHostId, "data-ui-id": ids.videoGallery, ref: containerRef, className: mergeStyles(videoGalleryOuterDivStyle, styles === null || styles === void 0 ? void 0 : styles.root, unselectable) }, videoGalleryLayout, React.createElement(Announcer, { announcementString: announcementString, ariaLive: "polite" }))); }; //# sourceMappingURL=VideoGallery.js.map