feat(stage) Add stage filmstrip (multiple participants on stage) (#11145)

See multiple participants on stage
Pin and unpin to stage
Automatic selection of participants to be displayed on the stage filmstrip based on dominant speaker changes
Make Filmstrip a reusable component. Used by MainFilmstrip (old functionality) and the new StageFilmstrip
Rename DominantSpeakerName to StageParticipantNameLabel
Active border now showed only for the dominant speaker (no longer for the pinned participant)
Hide video from the vertical filmstrip for the participants on stage
Update video constraints
Updated pinned indicator
This commit is contained in:
Robert Pintilii 2022-03-29 11:45:09 +03:00 committed by GitHub
parent 4db7312d53
commit c4db12cbd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1275 additions and 236 deletions

View File

@ -1308,6 +1308,10 @@ var config = {
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.
// disableResizable: false,
// // Disables the stage filmstrip
// // (displaying multiple participants on stage besides the vertical filmstrip)
// disableStageFilmstrip: false
// },
// Tile view related config options.
@ -1317,7 +1321,6 @@ var config = {
// numberOfVisibleTiles: 25
// },
// Specifies whether the chat emoticons are disabled or not
// disableChatSmileys: false,

View File

@ -2,7 +2,7 @@
* Various overrides outside of the filmstrip to style the app to support a
* tiled thumbnail experience.
*/
.tile-view {
.tile-view, .stage-filmstrip {
/**
* Let the avatar grow with the tile.
*/
@ -15,9 +15,9 @@
* Hide various features that should not be displayed while in tile view.
*/
#dominantSpeaker,
#filmstripLocalVideoThumbnail,
#largeVideoElementsContainer,
#sharedVideo {
#sharedVideo,
.stage-participant-label {
display: none;
}

View File

@ -1,4 +1,4 @@
.vertical-filmstrip .filmstrip {
.vertical-filmstrip span:not(.tile-view) .filmstrip {
&.hide-videos {
.remote-videos {
& > div {

View File

@ -722,6 +722,7 @@
},
"passwordDigitsOnly": "Up to {{number}} digits",
"passwordSetRemotely": "Set by another participant",
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
"skip": "Skip",
@ -1213,10 +1214,12 @@
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",
"pinToStage": "Pin to stage",
"remoteControl": "Start / Stop remote control",
"screenSharing": "Participant is sharing their screen",
"show": "Show on stage",
"showSelfView": "Show self view",
"unpinFromStage": "Unpin",
"videoMuted": "Camera disabled",
"videomute": "Participant has stopped the camera"
},

View File

@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip';
import { FILMSTRIP_BREAKPOINT, shouldDisplayStageFilmstrip } from '../../../react/features/filmstrip';
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
@ -414,7 +414,7 @@ export class VideoContainer extends LargeContainer {
const verticalFilmstripWidth = state['features/filmstrip'].width?.current;
if (currentLayout === LAYOUTS.TILE_VIEW) {
if (currentLayout === LAYOUTS.TILE_VIEW || shouldDisplayStageFilmstrip(state)) {
// We don't need to resize the large video since it won't be displayed and we'll resize when returning back
// to stage view.
return;

View File

@ -93,6 +93,7 @@ export { default as IconOutlook } from './office365.svg';
export { default as IconParticipants } from './participants.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPinParticipant } from './pin.svg';
export { default as IconPlane } from './paper-plane.svg';
export { default as IconPresentation } from './presentation.svg';
export { default as IconRaisedHand } from './raised-hand.svg';
@ -128,6 +129,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconTrash } from './trash.svg';
export { default as IconUnpin } from './unpin.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
export { default as IconVideoQualityHD } from './HD.svg';

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.69428 17.1265L5.98398 13.9439L7.92634 15.8862C8.57722 16.5371 9.63249 16.5371 10.2834 15.8862L10.8726 15.297C11.5235 14.6461 11.5235 13.5908 10.8726 12.9399L10.578 12.6453L11.2691 11.4935C11.396 11.2819 11.5684 11.1012 11.7737 10.9643L13.5832 9.75796L13.8189 9.99366C14.4698 10.6445 15.525 10.6445 16.1759 9.99366L16.7652 9.4044C17.416 8.75353 17.416 7.69825 16.7652 7.04738L10.8726 1.15482C10.2217 0.503949 9.16647 0.503949 8.5156 1.15482L7.92634 1.74408C7.27547 2.39495 7.27547 3.45023 7.92634 4.1011L8.27989 4.45465L7.3088 6.25811C7.13602 6.579 6.86281 6.83441 6.53102 6.98522L5.42201 7.48932L4.98006 7.04738C4.32919 6.39651 3.27391 6.39651 2.62304 7.04738L2.03379 7.63664C1.38291 8.28751 1.38291 9.34278 2.03379 9.99366L3.97615 11.936L0.793463 16.2257C0.603279 16.4821 0.629578 16.839 0.855274 17.0647C1.08097 17.2904 1.43794 17.3167 1.69428 17.1265ZM13.7956 7.6133L10.8492 9.57753C10.4386 9.8513 10.0938 10.2128 9.8399 10.636L8.47933 12.9037L9.69411 14.1184L9.10485 14.7077L3.2123 8.81515L3.80155 8.22589L5.0602 9.48454L7.2207 8.5025C7.88427 8.20088 8.43068 7.69006 8.77626 7.04828L10.3353 4.15299L9.10485 2.92259L9.69411 2.33333L15.5867 8.22589L14.9974 8.81515L13.7956 7.6133Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.36765 7.1488L8.36029 7.16248L3.11269 1.91488C2.78182 1.58401 2.24545 1.58392 1.91468 1.91469C1.58391 2.24546 1.58399 2.78183 1.91487 3.1127L7.01977 8.21761L6.422 8.48932L5.98005 8.04738C5.32918 7.39651 4.27391 7.39651 3.62303 8.04738L3.03378 8.63664C2.3829 9.28751 2.3829 10.3428 3.03378 10.9937L4.97614 12.936L1.79345 17.2257C1.60327 17.4821 1.62957 17.839 1.85526 18.0647C2.08096 18.2904 2.43793 18.3167 2.69427 18.1265L6.98397 14.9439L8.92633 16.8862C9.57721 17.5371 10.6325 17.5371 11.2834 16.8862L11.8726 16.297C12.5235 15.6461 12.5235 14.5908 11.8726 13.9399L11.578 13.6453L11.904 13.1019L16.8873 18.0851C17.2182 18.416 17.7545 18.4161 18.0853 18.0853C18.4161 17.7545 18.416 17.2182 18.0851 16.8873L13.0067 11.8089L13.0194 11.8005L11.8177 10.5988C11.8135 10.6017 11.8093 10.6045 11.8052 10.6074L9.57425 8.37644C9.57714 8.37231 9.58001 8.36817 9.58288 8.36403L8.36765 7.1488ZM13.2549 9.6404L14.7956 8.6133L15.9974 9.81515L16.5867 9.22589L10.6941 3.33333L10.1048 3.92259L11.3352 5.15299L10.4365 6.82203L9.20613 5.59163L9.27989 5.45465L8.92633 5.1011C8.27546 4.45023 8.27546 3.39495 8.92633 2.74408L9.51559 2.15482C10.1665 1.50395 11.2217 1.50395 11.8726 2.15482L17.7652 8.04738C18.416 8.69825 18.416 9.75353 17.7652 10.4044L17.1759 10.9937C16.525 11.6445 15.4698 11.6445 14.8189 10.9937L14.5832 10.758L14.4567 10.8422L13.2549 9.6404ZM9.47932 13.9037L10.6893 11.8871L8.27797 9.4758C8.25897 9.48488 8.23988 9.49378 8.22069 9.5025L6.06019 10.4845L4.80154 9.22589L4.21229 9.81515L10.1048 15.7077L10.6941 15.1184L9.47932 13.9037Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -3,6 +3,7 @@
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
@ -290,8 +291,16 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
* @returns {(Participant|undefined)}
*/
export function getPinnedParticipant(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { pinnedParticipant } = state;
const state = toState(stateful);
const { pinnedParticipant } = state['features/base/participants'];
const stageFilmstrip = isStageFilmstripEnabled(state);
if (stageFilmstrip) {
const { activeParticipants } = state['features/filmstrip'];
const id = activeParticipants.find(p => p.pinned)?.participantId;
return id ? getParticipantById(stateful, id) : undefined;
}
if (!pinnedParticipant) {
return undefined;

View File

@ -1,5 +1,6 @@
// @flow
import clsx from 'clsx';
import _ from 'lodash';
import React from 'react';
@ -11,7 +12,7 @@ import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip';
import { MainFilmstrip, StageFilmstrip, shouldDisplayStageFilmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
@ -55,7 +56,7 @@ const FULL_SCREEN_EVENTS = [
* @private
* @type {Object}
*/
const LAYOUT_CLASSNAMES = {
export const LAYOUT_CLASSNAMES = {
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
[LAYOUTS.TILE_VIEW]: 'tile-view',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
@ -95,13 +96,18 @@ type Props = AbstractProps & {
/**
* If lobby page is visible or not.
*/
_showLobby: boolean,
_showLobby: boolean,
/**
* If prejoin page is visible or not.
*/
_showPrejoin: boolean,
/**
* Whether or not the stage filmstrip should be displayed.
*/
_showStageFilmstrip: boolean,
dispatch: Function,
t: Function
}
@ -214,7 +220,8 @@ class Conference extends AbstractConference<Props, *> {
_notificationsVisible,
_overflowDrawer,
_showLobby,
_showPrejoin
_showPrejoin,
_showStageFilmstrip
} = this.props;
return (
@ -224,7 +231,7 @@ class Conference extends AbstractConference<Props, *> {
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove } >
<div
className = { _layoutClassName }
className = { clsx(_layoutClassName, _showStageFilmstrip && 'stage-filmstrip') }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
ref = { this._setBackground }>
@ -235,7 +242,8 @@ class Conference extends AbstractConference<Props, *> {
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo />
<Filmstrip />
{_showStageFilmstrip && <StageFilmstrip />}
<MainFilmstrip />
</div>
{ _showPrejoin || _showLobby || <Toolbox /> }
@ -395,7 +403,8 @@ function _mapStateToProps(state) {
_overflowDrawer: overflowDrawer,
_roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state)
_showPrejoin: isPrejoinPageVisible(state),
_showStageFilmstrip: shouldDisplayStageFilmstrip(state)
};
}

View File

@ -1,6 +1,7 @@
// @flow
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
@ -40,7 +41,7 @@ const useStyles = makeStyles(theme => {
*
* @returns {ReactElement|null}
*/
const DominantSpeakerName = () => {
const StageParticipantNameLabel = () => {
const classes = useStyles();
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
const nameToDisplay = largeVideoParticipant?.name;
@ -56,7 +57,11 @@ const DominantSpeakerName = () => {
if (showDisplayName && nameToDisplay && selectedId !== localId && !isTileView) {
return (
<div
className = { `${classes.badgeContainer}${toolboxVisible ? ` ${classes.containerElevated}` : ''}` }>
className = { clsx(
'stage-participant-label',
classes.badgeContainer,
toolboxVisible && classes.containerElevated
) }>
<DisplayNameBadge name = { nameToDisplay } />
</div>
);
@ -65,4 +70,4 @@ const DominantSpeakerName = () => {
return null;
};
export default DominantSpeakerName;
export default StageParticipantNameLabel;

View File

@ -2,4 +2,4 @@
export { default as DisplayName } from './DisplayName';
export { default as DisplayNamePrompt } from './DisplayNamePrompt';
export { default as DominantSpeakerName } from './DominantSpeakerName';
export { default as StageParticipantNameLabel } from './StageParticipantNameLabel';

View File

@ -118,3 +118,44 @@ export const SET_USER_FILMSTRIP_WIDTH = 'SET_USER_FILMSTRIP_WIDTH';
* }
*/
export const SET_USER_IS_RESIZING = 'SET_USER_IS_RESIZING';
/**
* The type of (redux) action which sets the dimensions of the thumbnails in stage filmstrip view.
*
* {
* type: SET_STAGE_FILMSTRIP_DIMENSIONS,
* dimensions: Object
* }
*/
export const SET_STAGE_FILMSTRIP_DIMENSIONS = 'SET_STAGE_FILMSTRIP_DIMENSIONS';
/**
* The type of Redux action which adds a participant to the active list
* (the participants displayed on the stage filmstrip).
* {
* type: ADD_STAGE_PARTICIPANT,
* participantId: string,
* pinned: boolean
* }
*/
export const ADD_STAGE_PARTICIPANT = 'ADD_STAGE_PARTICIPANT';
/**
* The type of Redux action which removes a participant from the active list
* (the participants displayed on the stage filmstrip).
* {
* type: REMOVE_STAGE_PARTICIPANT,
* participantId: string,
* }
*/
export const REMOVE_STAGE_PARTICIPANT = 'REMOVE_STAGE_PARTICIPANT';
/**
* The type of Redux action which sets the active participants list
* (the participants displayed on the stage filmstrip).
* {
* type: SET_STAGE_PARTICIPANTS,
* queue: Array<Object>
* }
*/
export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS';

View File

@ -11,8 +11,12 @@ import { shouldHideSelfView } from '../base/settings/functions.any';
import { getMaxColumnCount } from '../video-layout';
import {
ADD_STAGE_PARTICIPANT,
REMOVE_STAGE_PARTICIPANT,
SET_STAGE_PARTICIPANTS,
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_STAGE_FILMSTRIP_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_USER_IS_RESIZING,
@ -21,6 +25,7 @@ import {
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
MAX_ACTIVE_PARTICIPANTS,
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
@ -32,11 +37,12 @@ import {
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
calculateNotResponsiveTileViewDimensions,
calculateNonResponsiveTileViewDimensions,
calculateResponsiveTileViewDimensions,
calculateThumbnailSizeForHorizontalView,
calculateThumbnailSizeForVerticalView,
getNumberOfPartipantsForTileView,
getVerticalViewMaxWidth,
isFilmstripResizable,
showGridInVerticalView
} from './functions';
@ -46,9 +52,6 @@ export * from './actions.any';
/**
* Sets the dimensions of the tile view grid.
*
* @param {Object} dimensions - Whether the filmstrip is visible.
* @param {Object | Function} stateful - An object or function that can be
* resolved to Redux state using the {@code toState} function.
* @returns {Function}
*/
export function setTileViewDimensions() {
@ -70,7 +73,7 @@ export function setTileViewDimensions() {
columns,
rows
} = disableResponsiveTiles
? calculateNotResponsiveTileViewDimensions(state)
? calculateNonResponsiveTileViewDimensions(state)
: calculateResponsiveTileViewDimensions({
clientWidth,
clientHeight,
@ -142,8 +145,8 @@ export function setVerticalViewDimensions() {
clientWidth: filmstripWidth.current,
clientHeight,
disableTileEnlargement: false,
isVerticalFilmstrip: true,
maxColumns,
noHorizontalContainerMargin: true,
numberOfParticipants,
numberOfVisibleTiles
});
@ -230,6 +233,68 @@ export function setHorizontalViewDimensions() {
};
}
/**
* Sets the dimensions of the stage filmstrip tile view grid.
*
* @returns {Function}
*/
export function setStageFilmstripViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const {
disableResponsiveTiles,
disableTileEnlargement,
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
const maxColumns = getMaxColumnCount(state);
const {
height,
width,
columns,
rows
} = disableResponsiveTiles
? calculateNonResponsiveTileViewDimensions(state, true)
: calculateResponsiveTileViewDimensions({
clientWidth: clientWidth - verticalWidth,
clientHeight,
disableTileEnlargement,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth
= Math.min(clientWidth - TILE_VIEW_GRID_HORIZONTAL_MARGIN, columns * (TILE_HORIZONTAL_MARGIN + width))
+ (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN, thumbnailsTotalHeight);
dispatch({
type: SET_STAGE_FILMSTRIP_DIMENSIONS,
dimensions: {
gridDimensions: {
columns,
rows
},
thumbnailSize: {
height,
width
},
filmstripHeight,
filmstripWidth,
hasScroll
}
});
};
}
/**
* Emulates a click on the n-th video.
*
@ -313,3 +378,44 @@ export function setUserIsResizing(resizing: boolean) {
resizing
};
}
/**
* Add participant to the active participants list.
*
* @param {string} participantId - The Id of the participant to be added.
* @param {boolean?} pinned - Whether the participant is pinned or not.
* @returns {Object}
*/
export function addStageParticipant(participantId, pinned = false) {
return {
type: ADD_STAGE_PARTICIPANT,
participantId,
pinned
};
}
/**
* Remove participant from the active participants list.
*
* @param {string} participantId - The Id of the participant to be removed.
* @returns {Object}
*/
export function removeStageParticipant(participantId) {
return {
type: REMOVE_STAGE_PARTICIPANT,
participantId
};
}
/**
* Sets the active participants list.
*
* @param {Array<Object>} queue - The new list.
* @returns {Object}
*/
export function setStageParticipants(queue) {
return {
type: SET_STAGE_PARTICIPANTS,
queue
};
}

View File

@ -18,9 +18,10 @@ import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { CHAT_SIZE } from '../../../chat';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout';
import {
setFilmstripVisible,
setVisibleRemoteParticipants,
@ -30,19 +31,13 @@ import {
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
MIN_STAGE_VIEW_WIDTH,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE
TILE_VERTICAL_MARGIN
} from '../../constants';
import {
getVerticalViewMaxWidth,
isFilmstripResizable,
shouldRemoteVideosBeVisible,
showGridInVerticalView
shouldRemoteVideosBeVisible
} from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
@ -63,6 +58,11 @@ type Props = {
*/
_className: string,
/**
* Whether or not the chat is open.
*/
_chatOpen: boolean,
/**
* The current layout of the filmstrip.
*/
@ -138,6 +138,11 @@ type Props = {
*/
_rows: number,
/**
* Whether or not this is the stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The height of the thumbnail.
*/
@ -158,6 +163,11 @@ type Props = {
*/
_verticalFilmstripWidth: ?number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
@ -295,11 +305,13 @@ class Filmstrip extends PureComponent <Props, State> {
render() {
const filmstripStyle = { };
const {
_chatOpen,
_currentLayout,
_disableSelfView,
_resizableFilmstrip,
_verticalFilmstripWidth,
_stageFilmstrip,
_visible,
_verticalViewBackground,
_verticalViewGrid,
_verticalViewMaxWidth,
classes
@ -308,13 +320,20 @@ class Filmstrip extends PureComponent <Props, State> {
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
filmstripStyle.maxWidth = _verticalViewMaxWidth;
if (!_visible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
break;
}
case LAYOUTS.TILE_VIEW: {
if (_stageFilmstrip && _visible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px - ${_chatOpen ? CHAT_SIZE : 0}px)`;
}
break;
}
}
let toolbar = null;
@ -332,12 +351,12 @@ class Filmstrip extends PureComponent <Props, State> {
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
{
!tileViewActive && <div id = 'filmstripLocalVideoThumbnail'>
<Thumbnail
key = 'local' />
}
</div>
</div>
}
</div>
)}
{
@ -352,8 +371,7 @@ class Filmstrip extends PureComponent <Props, State> {
this.props._className,
classes.filmstrip,
_verticalViewGrid && 'no-vertical-padding',
_verticalFilmstripWidth + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
&& classes.filmstripBackground) }
_verticalViewBackground && classes.filmstripBackground) }
style = { filmstripStyle }>
{ toolbar }
{_resizableFilmstrip
@ -576,6 +594,7 @@ class Filmstrip extends PureComponent <Props, State> {
_remoteParticipantsLength,
_resizableFilmstrip,
_rows,
_stageFilmstrip,
_thumbnailHeight,
_thumbnailWidth,
_verticalViewGrid
@ -596,6 +615,7 @@ class Filmstrip extends PureComponent <Props, State> {
height = { _filmstripHeight }
initialScrollLeft = { 0 }
initialScrollTop = { 0 }
itemData = {{ stageFilmstrip: _stageFilmstrip }}
itemKey = { this._gridItemKey }
onItemsRendered = { this._onGridItemsRendered }
overscanRowCount = { 1 }
@ -759,129 +779,47 @@ class Filmstrip extends PureComponent <Props, State> {
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state, ownProps) {
const { _hasScroll = false } = ownProps;
const toolbarButtons = getToolbarButtons(state);
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
hasScroll: tileViewHasScroll,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const disableSelfView = shouldHideSelfView(state);
const _resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridDimensions = dimensions;
let _hasScroll = false;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const { clientWidth } = state['features/base/responsive-ui'];
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && (
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
const shouldReduceHeight = reduceHeight && isMobileBrowser();
let videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const className = `${remoteVideosVisible || _verticalViewGrid ? '' : 'hide-videos'} ${
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_hasScroll = Boolean(tileViewHasScroll);
if (_hasScroll) {
videosClassName += ' has-scroll';
}
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const {
remote,
remoteVideosContainer,
gridView,
hasScroll
} = state['features/filmstrip'].verticalViewDimensions;
_hasScroll = Boolean(hasScroll);
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
_thumbnailSize = gridView.thumbnailSize;
if (gridView.hasScroll) {
videosClassName += ' has-scroll';
}
} else {
_thumbnailSize = remote;
}
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
_hasScroll = Boolean(hasScroll);
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
}
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout,
_chatOpen: state['features/chat'].isOpen,
_disableSelfView: disableSelfView,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_thumbnailsReordered: enableThumbnailReordering,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_videosClassName: videosClassName,
_visible: visible,
_verticalViewGrid,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state)
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_videosClassName: videosClassName
};
}

View File

@ -0,0 +1,208 @@
// @flow
import React from 'react';
import { getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { connect } from '../../../base/redux';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE } from '../../constants';
import { isFilmstripResizable, showGridInVerticalView } from '../../functions.web';
import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* The height of the filmstrip.
*/
_filmstripHeight: number,
/**
* Whether the filmstrip has scroll or not.
*/
_hasScroll: boolean,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The participants in the call.
*/
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
};
const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
hasScroll: tileViewHasScroll,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const _resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridDimensions = dimensions;
let _hasScroll = false;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && (
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_hasScroll = Boolean(tileViewHasScroll);
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const {
remote,
remoteVideosContainer,
gridView,
hasScroll
} = state['features/filmstrip'].verticalViewDimensions;
_hasScroll = Boolean(hasScroll);
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
_thumbnailSize = gridView.thumbnailSize;
_hasScroll = gridView.hasScroll;
} else {
_thumbnailSize = remote;
}
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
_hasScroll = Boolean(hasScroll);
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
}
return {
_columns: gridDimensions.columns,
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_visible: visible,
_verticalViewGrid,
_verticalViewBackground: verticalFilmstripWidth.current + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
};
}
export default connect(_mapStateToProps)(MainFilmstrip);

View File

@ -0,0 +1,74 @@
/* @flow */
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { useSelector } from 'react-redux';
import { IconPinParticipant } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
import { getActiveParticipantsIds } from '../../functions.web';
/**
* The type of the React {@code Component} props of {@link PinnedIndicator}.
*/
type Props = {
/**
* The font-size for the icon.
*/
iconSize: number,
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string,
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
const useStyles = makeStyles(() => {
return {
pinnedIndicator: {
backgroundColor: 'rgba(0, 0, 0, .7)',
padding: '2px',
zIndex: 3,
display: 'inline-block',
borderRadius: '4px',
boxSizing: 'border-box'
}
};
});
/**
* Thumbnail badge showing that the participant would like to speak.
*
* @returns {ReactElement}
*/
const PinnedIndicator = ({
iconSize,
participantId,
tooltipPosition
}: Props) => {
const isPinned = useSelector(getActiveParticipantsIds).find(id => id === participantId);
const styles = useStyles();
if (!isPinned) {
return null;
}
return (
<div className = { styles.pinnedIndicator }>
<BaseIndicator
icon = { IconPinParticipant }
iconSize = { `${iconSize}px` }
tooltipKey = 'pinnedParticipant'
tooltipPosition = { tooltipPosition } />
</div>
);
};
export default PinnedIndicator;

View File

@ -0,0 +1,161 @@
// @flow
import React from 'react';
import { getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { connect } from '../../../base/redux';
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { getActiveParticipantsIds } from '../../functions';
import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* The height of the filmstrip.
*/
_filmstripHeight: number,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The participants in the call.
*/
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
};
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
_currentLayout = { LAYOUTS.TILE_VIEW }
_stageFilmstrip = { true } />
</span>
);
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
thumbnailSize
} = state['features/filmstrip'].stageFilmstripDimensions;
const gridDimensions = dimensions;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
return {
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipantsLength: activeParticipants.length,
_remoteParticipants: activeParticipants,
_resizableFilmstrip: false,
_rows: gridDimensions.rows,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_visible: visible,
_verticalViewGrid: false,
_verticalViewBackground: false
};
}
export default connect(_mapStateToProps)(StageFilmstrip);

View File

@ -28,6 +28,7 @@ import { hideGif, showGif } from '../../../gifs/actions';
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { addStageParticipant } from '../../actions.web';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
@ -36,10 +37,12 @@ import {
} from '../../constants';
import {
computeDisplayModeFromInput,
getActiveParticipantsIds,
getDisplayModeInput,
isVideoPlayable,
showGridInVerticalView
} from '../../functions';
import { isStageFilmstripEnabled } from '../../functions.web';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
@ -108,16 +111,17 @@ export type Props = {|
*/
_height: number,
/**
* Whether or not the participant is displayed on the stage filmstrip.
* Used to hide the video from the vertical filmstrip.
*/
_isActiveParticipant: boolean,
/**
* Indicates whether the thumbnail should be hidden or not.
*/
_isHidden: boolean,
/**
* Whether or not there is a pinned participant.
*/
_isAnyParticipantPinned: boolean,
/**
* Indicates whether audio only mode is enabled.
*/
@ -173,6 +177,11 @@ export type Props = {|
*/
_raisedHand: boolean,
/**
* Whether or not the stage filmstrip is disabled.
*/
_stageFilmstripDisabled: boolean,
/**
* The video object position for the participant.
*/
@ -208,6 +217,11 @@ export type Props = {|
*/
participantID: ?string,
/**
* Whether the tile is displayed in the stage filmstrip or not.
*/
stageFilmstrip: boolean,
/**
* Styles that will be set to the Thumbnail's main span element.
*/
@ -498,6 +512,13 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_hidePopover() {
const { _currentLayout } = this.props;
if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
this.setState({
isHovered: false
});
}
this.setState({
popoverVisible: false
});
@ -596,10 +617,14 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_onClick() {
const { _participant, dispatch } = this.props;
const { _participant, dispatch, _stageFilmstripDisabled } = this.props;
const { id, pinned } = _participant;
dispatch(pinParticipant(pinned ? null : id));
if (_stageFilmstripDisabled) {
dispatch(pinParticipant(pinned ? null : id));
} else {
dispatch(addStageParticipant(id, true));
}
}
_onMouseEnter: () => void;
@ -747,7 +772,6 @@ class Thumbnail extends Component<Props, State> {
_isDominantSpeakerDisabled,
_participant,
_currentLayout,
_isAnyParticipantPinned,
_raisedHand,
classes
} = this.props;
@ -758,17 +782,12 @@ class Thumbnail extends Component<Props, State> {
className += ` ${classes.raisedHand}`;
}
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
} else if (_isAnyParticipantPinned) {
if (_participant?.pinned) {
className += ` videoContainerFocused ${classes.activeSpeaker}`;
}
} else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
if (_currentLayout !== LAYOUTS.TILE_VIEW && _participant?.pinned) {
className += ' videoContainerFocused';
}
return className;
}
@ -876,8 +895,9 @@ class Thumbnail extends Component<Props, State> {
_localFlipX,
_participant,
_videoTrack,
_gifSrc,
classes,
_gifSrc
stageFilmstrip
} = this.props;
const { id } = _participant || {};
const { isHovered, popoverVisible } = this.state;
@ -914,7 +934,10 @@ class Thumbnail extends Component<Props, State> {
return (
<span
className = { containerClassName }
id = { local ? 'localVideoContainer' : `participant_${id}` }
id = { local
? `localVideoContainer${stageFilmstrip ? '_stage' : ''}`
: `participant_${id}${stageFilmstrip ? '_stage' : ''}`
}
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
@ -1017,7 +1040,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
const { participantID, stageFilmstrip } = ownProps;
const participant = getParticipantByIdOrUndefined(state, participantID);
const id = participant?.id;
@ -1027,7 +1050,7 @@ function _mapStateToProps(state, ownProps): Object {
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state);
let size = {};
let _isMobilePortrait = false;
const {
@ -1039,6 +1062,7 @@ function _mapStateToProps(state, ownProps): Object {
} = state['features/base/config'];
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
const activeParticipants = getActiveParticipantsIds(state);
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
@ -1079,12 +1103,26 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const {
stageFilmstripDimensions = {
thumbnailSize: {}
}
} = state['features/filmstrip'];
size = {
_width: width,
_height: height
_width: thumbnailSize?.width,
_height: thumbnailSize?.height
};
if (stageFilmstrip) {
const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
size = {
_width,
_height
};
}
break;
}
}
@ -1098,6 +1136,7 @@ function _mapStateToProps(state, ownProps): Object {
_defaultLocalDisplayName: defaultLocalDisplayName,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
_disableTileEnlargement: Boolean(disableTileEnlargement),
_isActiveParticipant: activeParticipants.find(pId => pId === participantID),
_isHidden: isLocal && iAmRecorder && !iAmSipGateway,
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
@ -1110,6 +1149,7 @@ function _mapStateToProps(state, ownProps): Object {
_localFlipX: Boolean(localFlipX),
_participant: participant,
_raisedHand: hasRaisedHand(participant),
_stageFilmstripDisabled: !isStageFilmstripEnabled(state),
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
_videoTrack,
...size,

View File

@ -11,6 +11,7 @@ import { LAYOUTS } from '../../../video-layout';
import { STATS_POPOVER_POSITION } from '../../constants';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import PinnedIndicator from './PinnedIndicator';
import RaisedHandIndicator from './RaisedHandIndicator';
import StatusIndicators from './StatusIndicators';
import VideoMenuTriggerButton from './VideoMenuTriggerButton';
@ -97,6 +98,10 @@ const ThumbnailTopIndicators = ({
return (
<>
<div className = { styles.container }>
<PinnedIndicator
iconSize = { _indicatorIconSize }
participantId = { participantId }
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
@ -119,6 +124,7 @@ const ThumbnailTopIndicators = ({
</div>
<div className = { styles.container }>
<VideoMenuTriggerButton
currentLayout = { currentLayout }
hidePopover = { hidePopover }
local = { local }
participantId = { participantId }

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react';
import { shouldComponentUpdate } from 'react-window';
import { getPinnedParticipant } from '../../../base/participants';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { showGridInVerticalView } from '../../functions';
import { showGridInVerticalView, getActiveParticipantsIds } from '../../functions';
import Thumbnail from './Thumbnail';
@ -25,16 +25,16 @@ type Props = {
*/
_horizontalOffset: number,
/**
* Whether or not there is a pinned participant.
*/
_isAnyParticipantPinned: boolean,
/**
* The ID of the participant associated with the Thumbnail.
*/
_participantID: ?string,
/**
* Whether or not the filmstrip is used a stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The index of the column in tile view.
*/
@ -82,7 +82,13 @@ class ThumbnailWrapper extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props;
const {
_disableSelfView,
_horizontalOffset = 0,
_participantID,
_stageFilmstrip,
style
} = this.props;
if (typeof _participantID !== 'string') {
return null;
@ -91,18 +97,18 @@ class ThumbnailWrapper extends Component<Props> {
if (_participantID === 'local') {
return _disableSelfView ? null : (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = 'local'
stageFilmstrip = { _stageFilmstrip }
style = { style } />);
}
return (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style } />);
}
}
@ -117,39 +123,60 @@ class ThumbnailWrapper extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants } = state['features/filmstrip'];
const remoteParticipantsLength = remoteParticipants.length;
const { remoteParticipants: remote } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const { testing = {} } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const _verticalViewGrid = showGridInVerticalView(state);
const _isAnyParticipantPinned = Boolean(getPinnedParticipant(state));
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const remoteParticipants = stageFilmstrip ? activeParticipants : remote;
const remoteParticipantsLength = remoteParticipants.length;
const localId = getLocalParticipant(state).id;
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || stageFilmstrip) {
const { columnIndex, rowIndex } = ownProps;
const { gridDimensions: dimensions = {}, thumbnailSize: size } = state['features/filmstrip'].tileViewDimensions;
const { gridView } = state['features/filmstrip'].verticalViewDimensions;
const gridDimensions = _verticalViewGrid ? gridView.gridDimensions : dimensions;
const thumbnailSize = _verticalViewGrid ? gridView.thumbnailSize : size;
const { tileViewDimensions, stageFilmstripDimensions, verticalViewDimensions } = state['features/filmstrip'];
const { gridView } = verticalViewDimensions;
let gridDimensions = tileViewDimensions.gridDimensions,
thumbnailSize = tileViewDimensions.thumbnailSize;
if (stageFilmstrip) {
gridDimensions = stageFilmstripDimensions.gridDimensions;
thumbnailSize = stageFilmstripDimensions.thumbnailSize;
} else if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
thumbnailSize = gridView.thumbnailSize;
}
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;
const { iAmRecorder } = state['features/base/config'];
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
const participantsLength = stageFilmstrip ? remoteParticipantsLength
: remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const partialLastRowParticipantsNumber = participantsLenght % columns;
const partialLastRowParticipantsNumber = participantsLength % columns;
if (partialLastRowParticipantsNumber > 0) {
horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
}
}
if (index > participantsLenght - 1) {
if (index > participantsLength - 1) {
return {};
}
if (stageFilmstrip) {
return {
_disableSelfView: disableSelfView,
_participantID: remoteParticipants[index] === localId ? 'local' : remoteParticipants[index],
_horizontalOffset: horizontalOffset,
_stageFilmstrip: stageFilmstrip
};
}
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = enableThumbnailReordering && !disableSelfView ? 0 : remoteParticipantsLength;
const remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
@ -158,15 +185,13 @@ function _mapStateToProps(state, ownProps) {
return {
_disableSelfView: disableSelfView,
_participantID: 'local',
_horizontalOffset: horizontalOffset,
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
_horizontalOffset: horizontalOffset
};
}
return {
_participantID: remoteParticipants[remoteIndex],
_horizontalOffset: horizontalOffset,
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
_horizontalOffset: horizontalOffset
};
}
@ -177,8 +202,7 @@ function _mapStateToProps(state, ownProps) {
}
return {
_participantID: remoteParticipants[index],
_isAnyParticipantPinned
_participantID: remoteParticipants[index]
};
}

View File

@ -6,6 +6,11 @@ import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../..
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Hide popover callback.
*/
@ -39,6 +44,7 @@ type Props = {
// eslint-disable-next-line no-confusing-arrow
const VideoMenuTriggerButton = ({
currentLayout,
hidePopover,
local,
participantId,
@ -50,6 +56,7 @@ const VideoMenuTriggerButton = ({
<span id = 'localvideomenu'>
<LocalVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
@ -59,6 +66,7 @@ const VideoMenuTriggerButton = ({
<span id = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
participantID = { participantId }
popoverVisible = { popoverVisible }

View File

@ -2,7 +2,9 @@
export { default as AudioMutedIndicator } from './AudioMutedIndicator';
export { default as Filmstrip } from './Filmstrip';
export { default as MainFilmstrip } from './MainFilmstrip';
export { default as ModeratorIndicator } from './ModeratorIndicator';
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
export { default as StageFilmstrip } from './StageFilmstrip';
export { default as StatusIndicators } from './StatusIndicators';
export { default as Thumbnail } from './Thumbnail';

View File

@ -284,3 +284,13 @@ export const MIN_STAGE_VIEW_WIDTH = 800;
*/
export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
+ SCROLL_SIZE + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
/**
* The time after which a participant should be removed from active participants.
*/
export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
/**
* The max number of participants to be displayed on the stage filmstrip.
*/
export const MAX_ACTIVE_PARTICIPANTS = 4;

View File

@ -18,6 +18,7 @@ import {
isRemoteTrackMuted
} from '../base/tracks/functions';
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
import { isSharingStatus } from '../shared-video/functions';
import {
getCurrentLayout,
getNotResponsiveTileViewGridDimensions,
@ -240,16 +241,18 @@ export function getNumberOfPartipantsForTileView(state) {
* disabled.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} - The dimensions.
*/
export function calculateNotResponsiveTileViewDimensions(state) {
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableTileEnlargement } = state['features/base/config'];
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
const filmstripWidth = getVerticalViewMaxWidth(state);
const size = calculateThumbnailSizeForTileView({
columns: c,
minVisibleRows,
clientWidth,
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: true
@ -289,7 +292,7 @@ export function calculateResponsiveTileViewDimensions({
clientWidth,
clientHeight,
disableTileEnlargement = false,
isVerticalFilmstrip = false,
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
@ -320,7 +323,7 @@ export function calculateResponsiveTileViewDimensions({
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: false,
isVerticalFilmstrip
noHorizontalContainerMargin
});
if (size) {
@ -389,12 +392,12 @@ export function calculateThumbnailSizeForTileView({
clientHeight,
disableResponsiveTiles = false,
disableTileEnlargement = false,
isVerticalFilmstrip = false
noHorizontalContainerMargin = false
}: Object) {
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
const minHeight = getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (isVerticalFilmstrip ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const initialWidth = viewWidth / columns;
let initialHeight = viewHeight / minVisibleRows;
@ -486,6 +489,7 @@ export function getVerticalFilmstripVisibleAreaWidth() {
*/
export function computeDisplayModeFromInput(input: Object) {
const {
isActiveParticipant,
isAudioOnly,
isCurrentlyOnLargeVideo,
isScreenSharing,
@ -495,7 +499,7 @@ export function computeDisplayModeFromInput(input: Object) {
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant) || isActiveParticipant)) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@ -519,6 +523,7 @@ export function computeDisplayModeFromInput(input: Object) {
export function getDisplayModeInput(props: Object, state: Object) {
const {
_currentLayout,
_isActiveParticipant,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isScreenSharing,
@ -530,6 +535,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
const { canPlayEventReceived } = state;
return {
isActiveParticipant: _isActiveParticipant,
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isAudioOnly: _isAudioOnly,
tileViewActive,
@ -613,7 +619,7 @@ export function isReorderingEnabled(state) {
const { testing = {} } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
return enableThumbnailReordering && isFilmstripScollVisible(state);
return enableThumbnailReordering && isFilmstripScrollVisible(state);
}
/**
@ -622,7 +628,7 @@ export function isReorderingEnabled(state) {
* @param {Object} state - The redux state.
* @returns {boolean} - True if the scroll is displayed and false otherwise.
*/
export function isFilmstripScollVisible(state) {
export function isFilmstripScrollVisible(state) {
const _currentLayout = getCurrentLayout(state);
let hasScroll = false;
@ -642,3 +648,43 @@ export function isFilmstripScollVisible(state) {
return hasScroll;
}
/**
* Gets the ids of the active participants.
*
* @param {Object} state - Redux state.
* @returns {Array<string>}
*/
export function getActiveParticipantsIds(state) {
const { activeParticipants } = state['features/filmstrip'];
return activeParticipants.map(p => p.participantId);
}
/**
* Get whether or not the stage filmstrip should be displayed.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function shouldDisplayStageFilmstrip(state) {
const { activeParticipants } = state['features/filmstrip'];
const { remoteScreenShares } = state['features/video-layout'];
const currentLayout = getCurrentLayout(state);
const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
return isStageFilmstripEnabled(state) && remoteScreenShares.length === 0 && !sharedVideo
&& activeParticipants.length > 1 && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
}
/**
* Whether the stage filmstrip is disabled or not.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isStageFilmstripEnabled(state) {
const { filmstrip } = state['features/base/config'];
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
}

View File

@ -1,26 +1,52 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import {
DOMINANT_SPEAKER_CHANGED,
getDominantSpeakerParticipant,
getLocalParticipant,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED } from '../base/responsive-ui';
import { SETTINGS_UPDATED } from '../base/settings';
import {
getCurrentLayout,
LAYOUTS
LAYOUTS,
setTileView
} from '../video-layout';
import { SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import { ADD_STAGE_PARTICIPANT, REMOVE_STAGE_PARTICIPANT, SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import {
addStageParticipant,
removeStageParticipant,
setFilmstripWidth,
setHorizontalViewDimensions,
setStageParticipants,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions';
import { DEFAULT_FILMSTRIP_WIDTH, MIN_STAGE_VIEW_WIDTH } from './constants';
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
import { isFilmstripResizable } from './functions.web';
import {
ACTIVE_PARTICIPANT_TIMEOUT,
DEFAULT_FILMSTRIP_WIDTH,
MAX_ACTIVE_PARTICIPANTS,
MIN_STAGE_VIEW_WIDTH
} from './constants';
import {
isFilmstripResizable,
updateRemoteParticipants,
updateRemoteParticipantsOnLeave
} from './functions';
import './subscriber';
import { getActiveParticipantsIds, isStageFilmstripEnabled } from './functions.web';
/**
* Map of timers.
*
* @type {Map}
*/
const timers = new Map();
/**
* The middleware of the feature Filmstrip.
@ -35,7 +61,7 @@ MiddlewareRegistry.register(store => next => action => {
updateRemoteParticipantsOnLeave(store, action.participant?.id);
}
const result = next(action);
let result;
switch (action.type) {
case CLIENT_RESIZED: {
@ -74,6 +100,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_JOINED: {
result = next(action);
updateRemoteParticipants(store, action.participant?.id);
break;
}
@ -82,12 +109,114 @@ MiddlewareRegistry.register(store => next => action => {
// TODO: This needs to be removed once the large video is Reactified.
VideoLayout.onLocalFlipXChanged();
}
if (action.settings?.disableSelfView) {
const state = store.getState();
const local = getLocalParticipant(state);
const activeParticipantsIds = getActiveParticipantsIds(state);
if (activeParticipantsIds.find(id => id === local.id)) {
store.dispatch(removeStageParticipant(local.id));
}
}
break;
}
case SET_USER_FILMSTRIP_WIDTH: {
VideoLayout.refreshLayout();
break;
}
case ADD_STAGE_PARTICIPANT: {
const { dispatch, getState } = store;
const { participantId, pinned } = action;
const state = getState();
const { activeParticipants } = state['features/filmstrip'];
let queue;
if (activeParticipants.find(p => p.participantId === participantId)) {
queue = activeParticipants.filter(p => p.participantId !== participantId);
queue.push({
participantId,
pinned
});
const tid = timers.get(participantId);
clearTimeout(tid);
} else if (activeParticipants.length < MAX_ACTIVE_PARTICIPANTS) {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
} else {
const notPinnedIndex = activeParticipants.findIndex(p => !p.pinned);
if (notPinnedIndex === -1) {
if (pinned) {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
queue.shift();
}
} else {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
queue.splice(notPinnedIndex, 1);
}
}
dispatch(setStageParticipants(queue));
if (!pinned) {
const timeoutId = setTimeout(() => dispatch(removeStageParticipant(participantId)),
ACTIVE_PARTICIPANT_TIMEOUT);
timers.set(participantId, timeoutId);
}
if (getCurrentLayout(state) === LAYOUTS.TILE_VIEW) {
dispatch(setTileView(false));
}
break;
}
case REMOVE_STAGE_PARTICIPANT: {
const state = store.getState();
const { participantId } = action;
const tid = timers.get(participantId);
clearTimeout(tid);
timers.delete(participantId);
const dominant = getDominantSpeakerParticipant(state);
if (participantId === dominant?.id) {
const timeoutId = setTimeout(() => store.dispatch(removeStageParticipant(participantId)),
ACTIVE_PARTICIPANT_TIMEOUT);
timers.set(participantId, timeoutId);
return;
}
break;
}
case DOMINANT_SPEAKER_CHANGED: {
const { id } = action.participant;
const state = store.getState();
const stageFilmstrip = isStageFilmstripEnabled(state);
const currentLayout = getCurrentLayout(state);
if (stageFilmstrip && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
store.dispatch(addStageParticipant(id));
}
break;
}
case PARTICIPANT_LEFT: {
const { id } = action.participant;
const activeParticipantsIds = getActiveParticipantsIds(store.getState());
if (activeParticipantsIds.find(pId => pId === id)) {
store.dispatch(removeStageParticipant(id));
}
break;
}
}
return result;
return result ?? next(action);
});

View File

@ -4,11 +4,14 @@ import { PARTICIPANT_LEFT } from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
REMOVE_STAGE_PARTICIPANT,
SET_STAGE_PARTICIPANTS,
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_REMOTE_PARTICIPANTS,
SET_STAGE_FILMSTRIP_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_USER_IS_RESIZING,
@ -18,6 +21,12 @@ import {
} from './actionTypes';
const DEFAULT_STATE = {
/**
* The list of participants to be displayed on the stage filmstrip.
*/
activeParticipants: [],
/**
* The indicator which determines whether the {@link Filmstrip} is enabled.
*
@ -57,6 +66,14 @@ const DEFAULT_STATE = {
*/
remoteParticipants: [],
/**
* The stage filmstrip view dimensions.
*
* @public
* @type {Object}
*/
stageFilmstripDimensions: {},
/**
* The tile view dimensions.
*
@ -223,6 +240,24 @@ ReducerRegistry.register(
isResizing: action.resizing
};
}
case SET_STAGE_FILMSTRIP_DIMENSIONS: {
return {
...state,
stageFilmstripDimensions: action.dimensions
};
}
case SET_STAGE_PARTICIPANTS: {
return {
...state,
activeParticipants: action.queue
};
}
case REMOVE_STAGE_PARTICIPANT: {
return {
...state,
activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId)
};
}
}
return state;

View File

@ -6,12 +6,14 @@ import { StateListenerRegistry } from '../base/redux';
import { clientResized } from '../base/responsive-ui';
import { shouldHideSelfView } from '../base/settings';
import { setFilmstripVisible } from '../filmstrip/actions';
import { selectParticipantInLargeVideo } from '../large-video/actions.any';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import {
setHorizontalViewDimensions,
setStageFilmstripViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions';
@ -19,7 +21,13 @@ import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_DRAWER_THRESHOLD
} from './constants';
import { isFilmstripResizable, isFilmstripScollVisible, updateRemoteParticipants } from './functions';
import {
isFilmstripResizable,
isFilmstripScrollVisible,
shouldDisplayStageFilmstrip,
updateRemoteParticipants
} from './functions';
import './subscriber.any';
@ -145,5 +153,39 @@ StateListenerRegistry.register(
* Listens for changes in the filmstrip scroll visibility.
*/
StateListenerRegistry.register(
/* selector */ state => isFilmstripScollVisible(state),
/* selector */ state => isFilmstripScrollVisible(state),
/* listener */ (_, store) => updateRemoteParticipants(store));
/**
* Listens for changes to determine the size of the stage filmstrip tiles.
*/
StateListenerRegistry.register(
/* selector */ state => {
return {
remoteScreenShares: state['features/video-layout'].remoteScreenShares.length,
length: state['features/filmstrip'].activeParticipants.length,
width: state['features/filmstrip'].width?.current,
visible: state['features/filmstrip'].visible,
clientWidth: state['features/base/responsive-ui'].clientWidth,
tileView: state['features/video-layout'].tileViewEnabled
};
},
/* listener */(_, store) => {
if (shouldDisplayStageFilmstrip(store.getState())) {
store.dispatch(setStageFilmstripViewDimensions());
}
}, {
deepEquals: true
});
/**
* Listens for changes in the active participants count determine the stage participant (when
* there's just one).
*/
StateListenerRegistry.register(
/* selector */ state => state['features/filmstrip'].activeParticipants.length,
/* listener */(length, store) => {
if (length <= 1) {
store.dispatch(selectParticipantInLargeVideo());
}
});

View File

@ -102,7 +102,14 @@ function _electLastVisibleRemoteVideo(tracks) {
* @returns {(string|undefined)}
*/
function _electParticipantInLargeVideo(state) {
// 1. If a participant is pinned, they will be shown in the LargeVideo
// 1. If there's a remote screenshare, pick the most recent one that was added to the conference.
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
if (remoteScreenShares?.length) {
return remoteScreenShares[remoteScreenShares.length - 1];
}
// 2. Next, if a participant is pinned, they will be shown in the LargeVideo
// (regardless of whether they are local or remote).
let participant = getPinnedParticipant(state);
@ -110,13 +117,6 @@ function _electParticipantInLargeVideo(state) {
return participant.id;
}
// 2. Next, pick the most recent remote screenshare that was added to the conference.
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
if (remoteScreenShares?.length) {
return remoteScreenShares[remoteScreenShares.length - 1];
}
// 3. Next, pick the dominant speaker (other than self).
participant = getDominantSpeakerParticipant(state);
if (participant && !participant.local) {

View File

@ -6,7 +6,7 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { DominantSpeakerName } from '../../display-name';
import { StageParticipantNameLabel } from '../../display-name';
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable } from '../../filmstrip';
import { getVerticalViewMaxWidth } from '../../filmstrip/functions.web';
import { SharedVideo } from '../../shared-video/components/web';
@ -175,7 +175,7 @@ class LargeVideo extends Component<Props> {
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|| <Captions /> }
{_showDominantSpeakerBadge && <DominantSpeakerName />}
{_showDominantSpeakerBadge && <StageParticipantNameLabel />}
</div>
);
}

View File

@ -73,13 +73,14 @@ export function getMaxColumnCount() {
* which rows will be added but no more columns.
*
* @param {Object} state - The redux store state.
* @param {number} width - Custom width to use for calculation.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} An object is return with the desired number of columns,
* rows, and visible rows (the rest should overflow) for the tile view layout.
*/
export function getNotResponsiveTileViewGridDimensions(state: Object) {
export function getNotResponsiveTileViewGridDimensions(state: Object, stageFilmstrip: boolean = false) {
const maxColumns = getMaxColumnCount(state);
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
const { activeParticipants } = state['features/filmstrip'];
const numberOfParticipants = stageFilmstrip ? activeParticipants.length : getNumberOfPartipantsForTileView(state);
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
@ -240,3 +241,15 @@ export function getVideoQualityForLargeVideo() {
return getVideoQualityForHeight(wrapper.clientHeight);
}
/**
* Gets the video quality level for the thumbnails in the stage filmstrip.
*
* @param {Object} state - Redux state.
* @returns {number}
*/
export function getVideoQualityForStageThumbnails(state) {
const height = state['features/filmstrip'].stageFilmstripDimensions?.thumbnailSize?.height;
return getVideoQualityForHeight(height);
}

View File

@ -12,6 +12,7 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { TRACK_REMOVED } from '../base/tracks';
import { SET_DOCUMENT_EDITING_STATUS } from '../etherpad';
import { isStageFilmstripEnabled } from '../filmstrip/functions.web';
import { isFollowMeActive } from '../follow-me';
import { SET_TILE_VIEW } from './actionTypes';
@ -68,11 +69,15 @@ MiddlewareRegistry.register(store => next => action => {
break;
// Things to update when tile view state changes
case SET_TILE_VIEW:
if (action.enabled && getPinnedParticipant(store)) {
case SET_TILE_VIEW: {
const state = store.getState();
const stageFilmstrip = isStageFilmstripEnabled(state);
if (action.enabled && !stageFilmstrip && getPinnedParticipant(state)) {
store.dispatch(pinParticipant(null));
}
break;
}
// Update the remoteScreenShares.
// Because of the debounce in the subscriber which updates the remoteScreenShares we need to handle

View File

@ -18,12 +18,14 @@ import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actio
import { getHideSelfView } from '../../../base/settings';
import { getLocalVideoTrack } from '../../../base/tracks';
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
import { LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ConnectionStatusButton from './ConnectionStatusButton';
import FlipLocalVideoButton from './FlipLocalVideoButton';
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
import TogglePinToStageButton from './TogglePinToStageButton';
/**
* The type of the React {@code Component} props of
@ -93,6 +95,11 @@ type Props = {
*/
_showLocalVideoFlipButton: boolean,
/**
* Whether to render the pin to stage button.
*/
_showPinToStage: boolean,
/**
* Invoked to obtain translated strings.
*/
@ -158,6 +165,7 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
_showConnectionInfo,
_showHideSelfViewButton,
_showLocalVideoFlipButton,
_showPinToStage,
buttonVisible,
classes,
hidePopover,
@ -183,8 +191,15 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
className = { _overflowDrawer ? classes.flipText : '' }
onClick = { hidePopover } />
}
{
_showPinToStage && <TogglePinToStageButton
className = { _overflowDrawer ? classes.flipText : '' }
noIcon = { true }
onClick = { hidePopover }
participantID = { _localParticipantId } />
}
{ isMobileBrowser()
&& <ConnectionStatusButton participantId = { _localParticipantId } />
&& <ConnectionStatusButton participantId = { _localParticipantId } />
}
</ContextMenuItemGroup>
</ContextMenu>
@ -254,11 +269,12 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const currentLayout = getCurrentLayout(state);
function _mapStateToProps(state, ownProps) {
const { currentLayout } = ownProps;
const localParticipant = getLocalParticipant(state);
const { disableLocalVideoFlip, disableSelfViewSettings } = state['features/base/config'];
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
@ -288,7 +304,8 @@ function _mapStateToProps(state) {
_showHideSelfViewButton: showHideSelfViewButton,
_overflowDrawer: overflowDrawer,
_localParticipantId: localParticipant.id,
_showConnectionInfo: showConnectionInfo
_showConnectionInfo: showConnectionInfo,
_showPinToStage: isStageFilmstripEnabled(state)
};
}

View File

@ -16,6 +16,7 @@ import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participant
import { isParticipantAudioMuted } from '../../../base/tracks';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { setVolume } from '../../../filmstrip/actions.web';
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { stopSharedVideo } from '../../../shared-video/actions.any';
@ -35,6 +36,7 @@ import {
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
TogglePinToStageButton,
VolumeSlider
} from './';
@ -144,6 +146,7 @@ const ParticipantContextMenu = ({
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerationSupported = useSelector(isAvModerationSupported());
const stageFilmstrip = useSelector(isStageFilmstripEnabled);
const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms = Object.values(useSelector(getBreakoutRooms));
@ -231,6 +234,12 @@ const ParticipantContextMenu = ({
}
}
if (stageFilmstrip) {
buttons2.push(<TogglePinToStageButton
key = 'pinToStage'
participantID = { _getCurrentParticipantId() } />);
}
if (!disablePrivateChat) {
buttons2.push(<PrivateMessageMenuButton
key = 'privateMessage'

View File

@ -14,7 +14,7 @@ import { getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ParticipantContextMenu from './ParticipantContextMenu';
@ -265,7 +265,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const { participantID, currentLayout } = ownProps;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _participantDisplayName = participant?.name;
@ -289,7 +289,6 @@ function _mapStateToProps(state, ownProps) {
}
}
const currentLayout = getCurrentLayout(state);
let _menuPosition;
switch (currentLayout) {

View File

@ -0,0 +1,62 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { IconPinParticipant, IconUnpin } from '../../../base/icons';
import { addStageParticipant, removeStageParticipant } from '../../../filmstrip/actions.web';
import { getActiveParticipantsIds } from '../../../filmstrip/functions';
type Props = {
/**
* Button text class name.
*/
className: string,
/**
* Whether the icon should be hidden or not.
*/
noIcon: boolean,
/**
* Click handler executed aside from the main action.
*/
onClick?: Function,
/**
* The ID for the participant on which the button will act.
*/
participantID: string
}
const TogglePinToStageButton = ({ className, noIcon = false, onClick, participantID }: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const isActive = Boolean(useSelector(getActiveParticipantsIds).find(p => p === participantID));
const _onClick = useCallback(() => {
dispatch(isActive
? removeStageParticipant(participantID)
: addStageParticipant(participantID, true));
onClick && onClick();
}, [ participantID, isActive ]);
const text = isActive
? t('videothumbnail.unpinFromStage')
: t('videothumbnail.pinToStage');
const icon = isActive ? IconUnpin : IconPinParticipant;
return (
<ContextMenuItem
accessibilityLabel = { text }
icon = { noIcon ? null : icon }
onClick = { _onClick }
text = { text }
textClassName = { className } />
);
};
export default TogglePinToStageButton;

View File

@ -13,6 +13,7 @@ export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog'
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as TogglePinToStageButton } from './TogglePinToStageButton';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';

View File

@ -9,9 +9,11 @@ import { getLocalParticipant, getParticipantCount } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks';
import { reportError } from '../base/util';
import { getActiveParticipantsIds } from '../filmstrip/functions.web';
import {
getVideoQualityForLargeVideo,
getVideoQualityForResizableFilmstripThumbnails,
getVideoQualityForStageThumbnails,
shouldDisplayTileView
} from '../video-layout';
@ -92,6 +94,17 @@ StateListenerRegistry.register(
}
);
/**
* Updates the receiver constraints when the stage participants change.
*/
StateListenerRegistry.register(
state => getActiveParticipantsIds(state).sort()
.join(),
(_, store) => {
_updateReceiverVideoConstraints(store);
}
);
/**
* StateListenerRegistry provides a reliable way of detecting changes to
* maxReceiverVideoQuality and preferredVideoQuality state and dispatching additional actions.
@ -219,6 +232,7 @@ function _updateReceiverVideoConstraints({ getState }) {
const tracks = state['features/base/tracks'];
const sourceNameSignaling = getSourceNameSignalingFeatureFlag(state);
const localParticipantId = getLocalParticipant(state).id;
const activeParticipantsIds = getActiveParticipantsIds(state);
let receiverConstraints;
@ -232,6 +246,7 @@ function _updateReceiverVideoConstraints({ getState }) {
};
const visibleRemoteTrackSourceNames = [];
let largeVideoSourceName;
const activeParticipantsSources = [];
if (visibleRemoteParticipants?.size) {
visibleRemoteParticipants.forEach(participantId => {
@ -239,6 +254,9 @@ function _updateReceiverVideoConstraints({ getState }) {
if (sourceName) {
visibleRemoteTrackSourceNames.push(sourceName);
if (activeParticipantsIds.find(id => id === participantId)) {
activeParticipantsSources.push(sourceName);
}
}
});
}
@ -277,10 +295,14 @@ function _updateReceiverVideoConstraints({ getState }) {
if (visibleRemoteTrackSourceNames?.length) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
visibleRemoteTrackSourceNames.forEach(sourceName => {
receiverConstraints.constraints[sourceName] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
const isStageParticipant = activeParticipantsSources.find(name => name === sourceName);
const quality = Math.min(maxFrameHeight, isStageParticipant
? stageParticipantsLevel : qualityLevel);
receiverConstraints.constraints[sourceName] = { 'maxHeight': quality };
});
}
@ -326,10 +348,14 @@ function _updateReceiverVideoConstraints({ getState }) {
if (visibleRemoteParticipants?.size > 0) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
visibleRemoteParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
const isStageParticipant = activeParticipantsIds.find(id => id === participantId);
const quality = Math.min(maxFrameHeight, isStageParticipant
? stageParticipantsLevel : qualityLevel);
receiverConstraints.constraints[participantId] = { 'maxHeight': quality };
});
}