@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
217 lines • 21.4 kB
JavaScript
// 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';
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));
}
// 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