Skip to content

Commit a283692

Browse files
[Gallery Layouts] Large Gallery (#3591)
* update logic to cap gallery participants * put leftovers into overflow * fix indexing issue * Change files * Duplicate change files for beta release * build files * Update packages/react-composites CallComposite browser test snapshots * update tests for new tile caps * add new layout * add large gallery dropdown * add internal component to video gallery * add logic for large gallery * Change files * Duplicate change files for beta release * add tile count maxing at 48 less if no room * build fixes * cleanup per comments * fix cc * Update packages/react-composites CallComposite browser test snapshots * fix cc * fix cc * Update logic to mix audtio video non dynamic * remove largeGallery API * make large gallery alpha only * Address comments * fix cc * Update packages/react-composites CallComposite browser test snapshots * fix comment --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d815973 commit a283692

16 files changed

Lines changed: 294 additions & 20 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "prerelease",
3+
"area": "feature",
4+
"workstream": "Gallery Layouts",
5+
"comment": "Introduces new Large gallery mode to the video gallery",
6+
"packageName": "@azure/communication-react",
7+
"email": "94866715+dmceachernmsft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "prerelease",
3+
"area": "feature",
4+
"workstream": "Gallery Layouts",
5+
"comment": "Introduces new Large gallery mode to the video gallery",
6+
"packageName": "@azure/communication-react",
7+
"email": "94866715+dmceachernmsft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}

common/config/babel/.babelrc.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ process.env['COMMUNICATION_REACT_FLAVOR'] !== 'beta' &&
7171
// Feature for custom video gallery layouts
7272
'gallery-layouts',
7373
// Feature image gallery
74-
'image-gallery'
74+
'image-gallery',
75+
// Feature for large Gallery layout
76+
'large-gallery'
7577
],
7678
// A list of in progress beta feature.
7779
// These features are still beta feature but "in progress"
@@ -81,7 +83,9 @@ process.env['COMMUNICATION_REACT_FLAVOR'] !== 'beta' &&
8183
// Do not use in production code.
8284
'in-progress-beta-feature-demo',
8385
// Feature for custom video gallery layouts
84-
'gallery-layouts'
86+
'gallery-layouts',
87+
// Feature for large gallery layout DO NOT REMOVE UNTIL SDK SUPPORTS 49 VIDEO STREAMS
88+
'large-gallery'
8589
],
8690
betaReleaseMode: process.env['COMMUNICATION_REACT_FLAVOR'] === 'beta-release',
8791
// A list of stabilized features.

packages/communication-react/review/beta/communication-react.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ export type CallCompositeIcons = {
578578
DefaultGalleryLayout?: JSX.Element;
579579
FocusedContentGalleryLayout?: JSX.Element;
580580
OverflowGalleryTop?: JSX.Element;
581+
LargeGalleryLayout?: JSX.Element;
581582
};
582583

583584
// @public
@@ -687,6 +688,7 @@ export interface CallCompositeStrings {
687688
moreButtonGalleryFocusedContentLayoutLabel?: string;
688689
moreButtonGalleryPositionToggleLabel?: string;
689690
moreButtonGallerySpeakerLayoutLabel?: string;
691+
moreButtonLargeGalleryDefaultLayoutLabel?: string;
690692
mutedMessage: string;
691693
networkReconnectMoreDetails: string;
692694
networkReconnectTitle: string;
@@ -2392,6 +2394,7 @@ export const DEFAULT_COMPOSITE_ICONS: {
23922394
DefaultGalleryLayout?: JSX.Element | undefined;
23932395
FocusedContentGalleryLayout?: JSX.Element | undefined;
23942396
OverflowGalleryTop?: JSX.Element | undefined;
2397+
LargeGalleryLayout?: JSX.Element | undefined;
23952398
ChevronLeft?: JSX.Element | undefined;
23962399
ControlBarChatButtonActive?: JSX.Element | undefined;
23972400
ControlBarChatButtonInactive?: JSX.Element | undefined;
@@ -4053,7 +4056,7 @@ export interface VideoBackgroundReplacementEffect extends BackgroundReplacementC
40534056
export const VideoGallery: (props: VideoGalleryProps) => JSX.Element;
40544057

40554058
// @public (undocumented)
4056-
export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | /* @conditional-compile-remove(gallery-layouts) */ 'speaker' | /* @conditional-compile-remove(gallery-layouts) */ 'focusedContent';
4059+
export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | /* @conditional-compile-remove(gallery-layouts) */ 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | /* @conditional-compile-remove(gallery-layouts) */ 'focusedContent';
40574060

40584061
// @public
40594062
export interface VideoGalleryLocalParticipant extends VideoGalleryParticipant {

packages/react-components/review/beta/react-components.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2332,7 +2332,7 @@ export interface _VideoEffectsItemStyles {
23322332
export const VideoGallery: (props: VideoGalleryProps) => JSX.Element;
23332333

23342334
// @public (undocumented)
2335-
export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | /* @conditional-compile-remove(gallery-layouts) */ 'speaker' | /* @conditional-compile-remove(gallery-layouts) */ 'focusedContent';
2335+
export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | /* @conditional-compile-remove(gallery-layouts) */ 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | /* @conditional-compile-remove(gallery-layouts) */ 'focusedContent';
23362336

23372337
// @public
23382338
export interface VideoGalleryLocalParticipant extends VideoGalleryParticipant {

packages/react-components/src/components/VideoGallery.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { VerticalGalleryStyles } from './VerticalGallery';
4242
import { SpeakerVideoLayout } from './VideoGallery/SpeakerVideoLayout';
4343
/* @conditional-compile-remove(gallery-layouts) */
4444
import { FocusedContentLayout } from './VideoGallery/FocusContentLayout';
45+
/* @conditional-compile-remove(gallery-layouts) */
46+
import { LargeGalleryLayout } from './VideoGallery/LargeGalleryLayout';
4547

4648
/**
4749
* @private
@@ -133,6 +135,7 @@ export type VideoGalleryLayout =
133135
| 'default'
134136
| 'floatingLocalVideo'
135137
| /* @conditional-compile-remove(gallery-layouts) */ 'speaker'
138+
| /* @conditional-compile-remove(large-gallery) */ 'largeGallery'
136139
| /* @conditional-compile-remove(gallery-layouts) */ 'focusedContent';
137140

138141
/**
@@ -683,6 +686,10 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => {
683686
if (layout === 'speaker') {
684687
return <SpeakerVideoLayout {...layoutProps} />;
685688
}
689+
/* @conditional-compile-remove(large-gallery) */
690+
if (layout === 'largeGallery') {
691+
return <LargeGalleryLayout {...layoutProps} />;
692+
}
686693
return <DefaultLayout {...layoutProps} />;
687694
}, [layout, layoutProps, /* @conditional-compile-remove(gallery-layouts) */ screenShareParticipant]);
688695

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import React, { useMemo, useRef, useState } from 'react';
5+
import { isNarrowWidth } from '../utils/responsive';
6+
/* @conditional-compile-remove(gallery-layouts) */
7+
import { isShortHeight } from '../utils/responsive';
8+
import { LayoutProps } from './Layout';
9+
import { OverflowGallery } from './OverflowGallery';
10+
import { GridLayout } from '../GridLayout';
11+
import { Stack } from '@fluentui/react';
12+
import { useOrganizedParticipants } from './utils/videoGalleryLayoutUtils';
13+
import { rootLayoutStyle } from './styles/DefaultLayout.styles';
14+
import { videoGalleryLayoutGap } from './styles/Layout.styles';
15+
/* @conditional-compile-remove(gallery-layouts) */
16+
import { VERTICAL_GALLERY_TILE_SIZE_REM } from './styles/VideoGalleryResponsiveVerticalGallery.styles';
17+
18+
/**
19+
* Props for {@link LargeGalleryLayout}.
20+
*
21+
* @private
22+
*/
23+
export type LargeGalleryProps = LayoutProps;
24+
25+
const DEFAULT_CHILDREN_PER_PAGE = 5;
26+
/* @conditional-compile-remove(gallery-layouts) */
27+
const REM_TO_PIXEL = 16;
28+
/* @conditional-compile-remove(gallery-layouts) */
29+
const LARGE_GALLERY_PARTICIPANT_CAP = 48;
30+
/**
31+
* VideoGallery Layout for when user is in a large meeting and wants to see more participants
32+
*
33+
* Caps the number of tiles that a participant can see in the grid to 49, Video and Audio.
34+
*
35+
* @private
36+
*/
37+
export const LargeGalleryLayout = (props: LargeGalleryProps): JSX.Element => {
38+
const {
39+
remoteParticipants = [],
40+
localParticipant,
41+
dominantSpeakers,
42+
localVideoComponent,
43+
screenShareComponent,
44+
onRenderRemoteParticipant,
45+
styles,
46+
maxRemoteVideoStreams,
47+
parentWidth,
48+
/* @conditional-compile-remove(gallery-layouts) */ parentHeight,
49+
pinnedParticipantUserIds = [],
50+
/* @conditional-compile-remove(vertical-gallery) */ overflowGalleryPosition = 'HorizontalBottom'
51+
} = props;
52+
53+
const isNarrow = parentWidth ? isNarrowWidth(parentWidth) : false;
54+
/* @conditional-compile-remove(gallery-layouts) */
55+
const isShort = parentHeight ? isShortHeight(parentHeight) : false;
56+
57+
const maxStreamsTrampoline = (): number => {
58+
/* @conditional-compile-remove(gallery-layouts) */
59+
return parentWidth && parentHeight
60+
? calculateMaxTilesInLargeGrid(parentWidth, parentHeight)
61+
: maxRemoteVideoStreams;
62+
return maxRemoteVideoStreams;
63+
};
64+
65+
// This is for tracking the number of children in the first page of overflow gallery.
66+
// This number will be used for the maxOverflowGalleryDominantSpeakers when organizing the remote participants.
67+
// We need to add the local participant to the pinned participant count so we are placing the speakers correctly.
68+
const childrenPerPage = useRef(DEFAULT_CHILDREN_PER_PAGE);
69+
const { gridParticipants, overflowGalleryParticipants } = useOrganizedParticipants({
70+
remoteParticipants,
71+
localParticipant,
72+
dominantSpeakers,
73+
maxRemoteVideoStreams: maxStreamsTrampoline(),
74+
isScreenShareActive: !!screenShareComponent,
75+
maxOverflowGalleryDominantSpeakers: screenShareComponent
76+
? childrenPerPage.current - ((pinnedParticipantUserIds.length + 1) % childrenPerPage.current)
77+
: childrenPerPage.current,
78+
/* @conditional-compile-remove(pinned-participants) */ pinnedParticipantUserIds,
79+
/* @conditional-compile-remove(gallery-layouts) */ layout: 'largeGallery'
80+
});
81+
let activeVideoStreams = 0;
82+
83+
const gridTiles = gridParticipants.map((p) => {
84+
return onRenderRemoteParticipant(
85+
p,
86+
maxRemoteVideoStreams && maxRemoteVideoStreams >= 0
87+
? p.videoStream?.isAvailable && activeVideoStreams++ < maxRemoteVideoStreams
88+
: p.videoStream?.isAvailable
89+
);
90+
});
91+
92+
/**
93+
* instantiate indexes available to render with indexes available that would be on first page
94+
*
95+
* For some components which do not strictly follow the order of the array, we might
96+
* re-render the initial tiles -> dispose them -> create new tiles, we need to take care of
97+
* this case when those components are here
98+
*/
99+
const [indexesToRender, setIndexesToRender] = useState<number[]>([]);
100+
101+
const overflowGalleryTiles = overflowGalleryParticipants.map((p, i) => {
102+
return onRenderRemoteParticipant(
103+
p,
104+
maxRemoteVideoStreams && maxRemoteVideoStreams >= 0
105+
? p.videoStream?.isAvailable && indexesToRender.includes(i) && activeVideoStreams++ < maxRemoteVideoStreams
106+
: p.videoStream?.isAvailable
107+
);
108+
});
109+
110+
if (localVideoComponent) {
111+
gridTiles.push(localVideoComponent);
112+
}
113+
114+
const overflowGallery = useMemo(() => {
115+
if (overflowGalleryTiles.length === 0) {
116+
return null;
117+
}
118+
return (
119+
<OverflowGallery
120+
isNarrow={isNarrow}
121+
/* @conditional-compile-remove(gallery-layouts) */
122+
isShort={isShort}
123+
shouldFloatLocalVideo={false}
124+
overflowGalleryElements={overflowGalleryTiles}
125+
horizontalGalleryStyles={styles?.horizontalGallery}
126+
/* @conditional-compile-remove(vertical-gallery) */
127+
verticalGalleryStyles={styles?.verticalGallery}
128+
/* @conditional-compile-remove(vertical-gallery) */
129+
overflowGalleryPosition={overflowGalleryPosition}
130+
onFetchTilesToRender={setIndexesToRender}
131+
onChildrenPerPageChange={(n: number) => {
132+
childrenPerPage.current = n;
133+
}}
134+
/>
135+
);
136+
}, [
137+
isNarrow,
138+
/* @conditional-compile-remove(gallery-layouts) */ isShort,
139+
overflowGalleryTiles,
140+
styles?.horizontalGallery,
141+
/* @conditional-compile-remove(vertical-gallery) */ overflowGalleryPosition,
142+
setIndexesToRender,
143+
/* @conditional-compile-remove(vertical-gallery) */ styles?.verticalGallery
144+
]);
145+
146+
return (
147+
<Stack
148+
/* @conditional-compile-remove(vertical-gallery) */
149+
horizontal={overflowGalleryPosition === 'VerticalRight'}
150+
styles={rootLayoutStyle}
151+
tokens={videoGalleryLayoutGap}
152+
>
153+
{
154+
/* @conditional-compile-remove(gallery-layouts) */ props.overflowGalleryPosition === 'HorizontalTop' ? (
155+
overflowGallery
156+
) : (
157+
<></>
158+
)
159+
}
160+
{screenShareComponent ? (
161+
screenShareComponent
162+
) : (
163+
<GridLayout key="grid-layout" styles={styles?.gridLayout}>
164+
{gridTiles}
165+
</GridLayout>
166+
)}
167+
{overflowGalleryTrampoline(
168+
overflowGallery,
169+
/* @conditional-compile-remove(gallery-layouts) */ props.overflowGalleryPosition
170+
)}
171+
</Stack>
172+
);
173+
};
174+
175+
const overflowGalleryTrampoline = (
176+
gallery: JSX.Element | null,
177+
galleryPosition?: 'HorizontalBottom' | 'VerticalRight' | 'HorizontalTop'
178+
): JSX.Element | null => {
179+
/* @conditional-compile-remove(gallery-layouts) */
180+
return galleryPosition !== 'HorizontalTop' ? gallery : <></>;
181+
return gallery;
182+
};
183+
184+
/* @conditional-compile-remove(gallery-layouts) */
185+
const calculateMaxTilesInLargeGrid = (parentWidth: number, parentHeight: number): number => {
186+
const xAxisTiles = Math.floor(parentWidth / (VERTICAL_GALLERY_TILE_SIZE_REM.width * REM_TO_PIXEL));
187+
const yAxisTiles = Math.floor(parentHeight / (VERTICAL_GALLERY_TILE_SIZE_REM.minHeight * REM_TO_PIXEL));
188+
return xAxisTiles * yAxisTiles < LARGE_GALLERY_PARTICIPANT_CAP
189+
? xAxisTiles * yAxisTiles
190+
: LARGE_GALLERY_PARTICIPANT_CAP;
191+
};

packages/react-components/src/components/VideoGallery/utils/videoGalleryLayoutUtils.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export interface OrganizedParticipantsResult {
3333
}
3434

3535
const DEFAULT_MAX_OVERFLOW_GALLERY_DOMINANT_SPEAKERS = 6;
36+
const DEFAULT_MAX_VIDEO_SREAMS = 4;
37+
/* @conditional-compile-remove(gallery-layouts) */
38+
const MAX_GRID_PARTICIPANTS_NOT_LARGE_GALLERY = 9;
3639

3740
const _useOrganizedParticipants = (props: OrganizedParticipantsArgs): OrganizedParticipantsResult => {
3841
const visibleGridParticipants = useRef<VideoGalleryRemoteParticipant[]>([]);
@@ -42,14 +45,26 @@ const _useOrganizedParticipants = (props: OrganizedParticipantsArgs): OrganizedP
4245
remoteParticipants = [],
4346
localParticipant,
4447
dominantSpeakers = [],
45-
maxRemoteVideoStreams,
48+
maxRemoteVideoStreams = DEFAULT_MAX_VIDEO_SREAMS,
4649
maxOverflowGalleryDominantSpeakers = DEFAULT_MAX_OVERFLOW_GALLERY_DOMINANT_SPEAKERS,
4750
isScreenShareActive = false,
4851
pinnedParticipantUserIds = [],
4952
/* @conditional-compile-remove(gallery-layouts) */
5053
layout
5154
} = props;
5255

56+
const calculateMaxRemoteVideoStreams = (): number => {
57+
/* @conditional-compile-remove(gallery-layouts) */
58+
if (maxRemoteVideoStreams > MAX_GRID_PARTICIPANTS_NOT_LARGE_GALLERY) {
59+
return MAX_GRID_PARTICIPANTS_NOT_LARGE_GALLERY;
60+
} else {
61+
return maxRemoteVideoStreams;
62+
}
63+
return maxRemoteVideoStreams;
64+
};
65+
66+
const maxRemoteVideoStreamsToUse = calculateMaxRemoteVideoStreams();
67+
5368
const videoParticipants = remoteParticipants.filter((p) => p.videoStream?.isAvailable);
5469

5570
const participantsToSortTrampoline = (): VideoGalleryRemoteParticipant[] => {
@@ -65,8 +80,8 @@ const _useOrganizedParticipants = (props: OrganizedParticipantsArgs): OrganizedP
6580
participants: participantsToSortTrampoline(),
6681
dominantSpeakers,
6782
lastVisibleParticipants: visibleGridParticipants.current,
68-
maxDominantSpeakers: maxRemoteVideoStreams as number
69-
}).slice(0, maxRemoteVideoStreams);
83+
maxDominantSpeakers: maxRemoteVideoStreamsToUse
84+
}).slice(0, maxRemoteVideoStreamsToUse);
7085

7186
/* @conditional-compile-remove(gallery-layouts) */
7287
const dominantSpeakerToGrid =
@@ -112,16 +127,16 @@ const _useOrganizedParticipants = (props: OrganizedParticipantsArgs): OrganizedP
112127
/* @conditional-compile-remove(PSTN-calls) */ /* @conditional-compile-remove(one-to-n-calling) */
113128
return visibleGridParticipants.current.length > 0
114129
? visibleGridParticipants.current
115-
: visibleOverflowGalleryParticipants.current.length > (maxRemoteVideoStreams as number)
116-
? visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreams)
117-
: visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreams).concat(callingParticipants);
130+
: visibleOverflowGalleryParticipants.current.length > maxRemoteVideoStreamsToUse
131+
? visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreamsToUse)
132+
: visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreamsToUse).concat(callingParticipants);
118133
return visibleGridParticipants.current.length > 0
119134
? visibleGridParticipants.current
120-
: visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreams);
135+
: visibleOverflowGalleryParticipants.current.slice(0, maxRemoteVideoStreamsToUse);
121136
}, [
122137
/* @conditional-compile-remove(PSTN-calls) */ /* @conditional-compile-remove(one-to-n-calling) */ callingParticipants,
123138
isScreenShareActive,
124-
maxRemoteVideoStreams
139+
maxRemoteVideoStreamsToUse
125140
]);
126141

127142
const gridParticipants = getGridParticipants();
@@ -153,18 +168,18 @@ const _useOrganizedParticipants = (props: OrganizedParticipantsArgs): OrganizedP
153168
/* @conditional-compile-remove(PSTN-calls) */ /* @conditional-compile-remove(one-to-n-calling) */
154169
return visibleGridParticipants.current.length > 0
155170
? visibleOverflowGalleryParticipants.current.concat(callingParticipants)
156-
: visibleOverflowGalleryParticipants.current.length > (maxRemoteVideoStreams as number)
157-
? visibleOverflowGalleryParticipants.current.slice(maxRemoteVideoStreams).concat(callingParticipants)
171+
: visibleOverflowGalleryParticipants.current.length > maxRemoteVideoStreamsToUse
172+
? visibleOverflowGalleryParticipants.current.slice(maxRemoteVideoStreamsToUse).concat(callingParticipants)
158173
: [];
159174
return visibleGridParticipants.current.length > 0
160175
? visibleOverflowGalleryParticipants.current
161-
: visibleOverflowGalleryParticipants.current.slice(maxRemoteVideoStreams);
176+
: visibleOverflowGalleryParticipants.current.slice(maxRemoteVideoStreamsToUse);
162177
}
163178
}, [
164179
/* @conditional-compile-remove(PSTN-calls) */ /* @conditional-compile-remove(one-to-n-calling) */ callingParticipants,
165180
isScreenShareActive,
166181
localParticipant,
167-
maxRemoteVideoStreams
182+
maxRemoteVideoStreamsToUse
168183
]);
169184

170185
const overflowGalleryParticipants = getOverflowGalleryRemoteParticipants();

0 commit comments

Comments
 (0)