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
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';
/* @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