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:
parent
4db7312d53
commit
c4db12cbd6
|
@ -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,
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.vertical-filmstrip .filmstrip {
|
||||
.vertical-filmstrip span:not(.tile-view) .filmstrip {
|
||||
&.hide-videos {
|
||||
.remote-videos {
|
||||
& > div {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue