feat: Add screenshare filmstrip (#11714)

Add new screen share layout with resizable top panel
Only enable new layout in large meetings (min 50 participants - configurable)
This commit is contained in:
Robert Pintilii 2022-06-29 16:59:49 +03:00 committed by GitHub
parent bac1347961
commit 21cf7f23c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 740 additions and 139 deletions

View File

@ -1205,7 +1205,8 @@ var config = {
// 'transcribing',
// 'video-quality',
// 'insecure-room',
// 'highlight-moment'
// 'highlight-moment',
// 'top-panel-toggle'
// ]
// },
@ -1399,7 +1400,14 @@ var config = {
// // Disables the stage filmstrip
// // (displaying multiple participants on stage besides the vertical filmstrip)
// disableStageFilmstrip: false
// disableStageFilmstrip: false,
// // Disables the top panel (only shown when a user is sharing their screen).
// disableTopPanel: false,
// // The minimum number of participants that must be in the call for
// // the top panel layout to be used.
// minParticipantCountForTopPanel: 50
// },
// Tile view related config options.

View File

@ -1033,6 +1033,7 @@
"termsView": {
"header": "Terms"
},
"toggleTopPanelLabel": "Toggle top panel",
"toolbar": {
"Settings": "Settings",
"accessibilityLabel": {

12
package-lock.json generated
View File

@ -7888,9 +7888,9 @@
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@ -26086,9 +26086,9 @@
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}

View File

@ -8,6 +8,7 @@ export const CONFERENCE_INFO = {
'e2ee',
'transcribing',
'video-quality',
'insecure-room'
'insecure-room',
'top-panel-toggle'
]
};

View File

@ -11,7 +11,7 @@ import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { MainFilmstrip, StageFilmstrip } from '../../../filmstrip';
import { MainFilmstrip, StageFilmstrip, ScreenshareFilmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
@ -239,6 +239,7 @@ class Conference extends AbstractConference<Props, *> {
{
_showPrejoin || _showLobby || (<>
<StageFilmstrip />
<ScreenshareFilmstrip />
<MainFilmstrip />
</>)
}

View File

@ -20,6 +20,7 @@ import InsecureRoomNameLabel from './InsecureRoomNameLabel';
import ParticipantsCount from './ParticipantsCount';
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
import SubjectText from './SubjectText';
import ToggleTopPanelLabel from './ToggleTopPanelLabel';
/**
* The type of the React {@code Component} props of {@link Subject}.
@ -82,6 +83,10 @@ const COMPONENTS = [
{
Component: InsecureRoomNameLabel,
id: 'insecure-room'
},
{
Component: ToggleTopPanelLabel,
id: 'top-panel-toggle'
}
];

View File

@ -0,0 +1,31 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
// @ts-ignore
import { IconMenuDown } from '../../../base/icons';
// @ts-ignore
import { Label } from '../../../base/label';
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
// @ts-ignore
import { setTopPanelVisible } from '../../../filmstrip/actions.web';
const ToggleTopPanelLabel = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const topPanelHidden = !useSelector((state: any) => state['features/filmstrip'].topPanelVisible);
const onClick = useCallback(() => {
dispatch(setTopPanelVisible(true));
}, []);
return topPanelHidden && (<Tooltip
content={t('toggleTopPanelLabel') }
position = { 'bottom' }>
<Label
icon={IconMenuDown}
onClick = { onClick }/>
</Tooltip>);
};
export default ToggleTopPanelLabel;

View File

@ -92,6 +92,15 @@ export const SET_VOLUME = 'SET_VOLUME';
*/
export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS';
/**
* The type of action which sets the height for the top panel filmstrip.
* {
* type: SET_FILMSTRIP_HEIGHT,
* height: number
* }
*/
export const SET_FILMSTRIP_HEIGHT = 'SET_FILMSTRIP_HEIGHT';
/**
* The type of action which sets the width for the vertical filmstrip.
* {
@ -101,6 +110,15 @@ export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS'
*/
export const SET_FILMSTRIP_WIDTH = 'SET_FILMSTRIP_WIDTH';
/**
* The type of action which sets the height for the top panel filmstrip (user resized).
* {
* type: SET_USER_FILMSTRIP_HEIGHT,
* height: number
* }
*/
export const SET_USER_FILMSTRIP_HEIGHT = 'SET_USER_FILMSTRIP_HEIGHT';
/**
* The type of action which sets the width for the vertical filmstrip (user resized).
* {
@ -187,3 +205,19 @@ export const TOGGLE_PIN_STAGE_PARTICIPANT = 'TOGGLE_PIN_STAGE_PARTICIPANT';
* }
*/
export const CLEAR_STAGE_PARTICIPANTS = 'CLEAR_STAGE_PARTICIPANTS';
/**
* The type of Redux action which sets the dimensions of the screenshare tile.
* {
* type: SET_SCREENSHARING_TILE_DIMENSIONS
* }
*/
export const SET_SCREENSHARING_TILE_DIMENSIONS = 'SET_SCREENSHARING_TILE_DIMENSIONS';
/**
* The type of Redux action which sets the visibility of the top panel.
* {
* type: SET_TOP_PANEL_VISIBILITY
* }
*/
export const SET_TOP_PANEL_VISIBILITY = 'SET_TOP_PANEL_VISIBILITY';

View File

@ -25,7 +25,11 @@ import {
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
TOGGLE_PIN_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS
CLEAR_STAGE_PARTICIPANTS,
SET_SCREENSHARING_TILE_DIMENSIONS,
SET_USER_FILMSTRIP_HEIGHT,
SET_FILMSTRIP_HEIGHT,
SET_TOP_PANEL_VISIBILITY
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
@ -33,11 +37,13 @@ import {
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_SMALL,
TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
TILE_VIEW_GRID_VERTICAL_MARGIN,
TOP_FILMSTRIP_HEIGHT,
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
@ -48,6 +54,7 @@ import {
getNumberOfPartipantsForTileView,
getVerticalViewMaxWidth,
isFilmstripResizable,
isStageFilmstripTopPanel,
showGridInVerticalView
} from './functions';
import { isStageFilmstripAvailable } from './functions.web';
@ -270,7 +277,7 @@ export function setStageFilmstripViewDimensions() {
const {
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const { visible, topPanelHeight } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
@ -280,6 +287,7 @@ export function setStageFilmstripViewDimensions() {
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const topPanel = isStageFilmstripTopPanel(state);
const {
height,
@ -288,12 +296,13 @@ export function setStageFilmstripViewDimensions() {
rows
} = calculateResponsiveTileViewDimensions({
clientWidth: availableWidth,
clientHeight,
clientHeight: topPanel ? topPanelHeight?.current || TOP_FILMSTRIP_HEIGHT : clientHeight,
disableTileEnlargement: false,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
numberOfVisibleTiles,
minTileHeight: topPanel ? TILE_MIN_HEIGHT_SMALL : null
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
@ -368,6 +377,22 @@ export function setVolume(participantId: string, volume: number) {
};
}
/**
* Sets the top filmstrip's height.
*
* @param {number} height - The new height of the filmstrip.
* @returns {{
* type: SET_FILMSTRIP_HEIGHT,
* height: number
* }}
*/
export function setFilmstripHeight(height: number) {
return {
type: SET_FILMSTRIP_HEIGHT,
height
};
}
/**
* Sets the filmstrip's width.
*
@ -384,6 +409,22 @@ export function setFilmstripWidth(width: number) {
};
}
/**
* Sets the filmstrip's height and the user preferred height.
*
* @param {number} height - The new height of the filmstrip.
* @returns {{
* type: SET_USER_FILMSTRIP_WIDTH,
* height: number
* }}
*/
export function setUserFilmstripHeight(height: number) {
return {
type: SET_USER_FILMSTRIP_HEIGHT,
height
};
}
/**
* Sets the filmstrip's width and the user preferred width.
*
@ -490,3 +531,45 @@ export function clearStageParticipants() {
type: CLEAR_STAGE_PARTICIPANTS
};
}
/**
* Set the screensharing tile dimensions.
*
* @returns {Object}
*/
export function setScreensharingTileDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { visible, topPanelHeight, topPanelVisible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const availableWidth = clientWidth - verticalWidth;
const topPanel = isStageFilmstripTopPanel(state) && topPanelVisible;
const availableHeight = clientHeight - (topPanel ? topPanelHeight?.current || TOP_FILMSTRIP_HEIGHT : 0);
dispatch({
type: SET_SCREENSHARING_TILE_DIMENSIONS,
dimensions: {
filmstripHeight: availableHeight,
filmstripWidth: availableWidth,
thumbnailSize: {
width: availableWidth - TILE_HORIZONTAL_MARGIN,
height: availableHeight - TILE_VERTICAL_MARGIN
}
}
});
};
}
/**
* Sets the visibility of the top panel.
*
* @param {boolean} visible - Whether it should be visible or not.
* @returns {Object}
*/
export function setTopPanelVisible(visible) {
return {
type: SET_TOP_PANEL_VISIBILITY,
visible
};
}

View File

@ -23,20 +23,26 @@ import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.we
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
setFilmstripVisible,
setVisibleRemoteParticipants,
setUserFilmstripHeight,
setUserFilmstripWidth,
setUserIsResizing
setUserIsResizing,
setTopPanelVisible,
setVisibleRemoteParticipants
} from '../../actions';
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
FILMSTRIP_TYPE,
MIN_STAGE_VIEW_HEIGHT,
MIN_STAGE_VIEW_WIDTH,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN
TILE_VERTICAL_MARGIN,
TOP_FILMSTRIP_HEIGHT
} from '../../constants';
import {
getVerticalViewMaxWidth,
shouldRemoteVideosBeVisible
shouldRemoteVideosBeVisible,
isStageFilmstripTopPanel
} from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
@ -112,11 +118,21 @@ type Props = {
*/
_localScreenShare: Object,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_mainFilmstripVisible: boolean,
/**
* The maximum width of the vertical filmstrip.
*/
_maxFilmstripWidth: number,
/**
* The maximum height of the top panel.
*/
_maxTopPanelHeight: number,
/**
* The participants in the call.
*/
@ -137,11 +153,6 @@ type Props = {
*/
_rows: number,
/**
* Whether or not this is the stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The height of the thumbnail.
*/
@ -157,6 +168,26 @@ type Props = {
*/
_thumbnailsReordered: Boolean,
/**
* Whether or not the filmstrip is top panel.
*/
_topPanelFilmstrip: boolean,
/**
* The max height of the top panel.
*/
_topPanelMaxHeight: number,
/**
* The height of the top panel (user resized).
*/
_topPanelHeight: ?number,
/**
* Whether or not the top panel is visible.
*/
_topPanelVisible: boolean,
/**
* The width of the vertical filmstrip (user resized).
*/
@ -182,11 +213,6 @@ type Props = {
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean,
/**
* An object containing the CSS classes.
*/
@ -197,6 +223,11 @@ type Props = {
*/
dispatch: Dispatch<any>,
/**
* The type of filmstrip to be displayed.
*/
filmstripType: string,
/**
* Invoked to obtain translated strings.
*/
@ -218,7 +249,12 @@ type State = {
/**
* Initial filmstrip width on drag handle mouse down.
*/
dragFilmstripWidth: ?number
dragFilmstripWidth: ?number,
/**
* Initial top panel height on drag handle mouse down.
*/
dragFilmstripHeight: ?number
}
/**
@ -307,25 +343,45 @@ class Filmstrip extends PureComponent <Props, State> {
_currentLayout,
_disableSelfView,
_localScreenShare,
_mainFilmstripVisible,
_resizableFilmstrip,
_stageFilmstrip,
_visible,
_topPanelFilmstrip,
_topPanelMaxHeight,
_topPanelVisible,
_verticalViewBackground,
_verticalViewGrid,
_verticalViewMaxWidth,
classes
classes,
filmstripType
} = this.props;
const { isMouseDown } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && _stageFilmstrip) {
if (_visible) {
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.STAGE) {
if (_topPanelFilmstrip) {
filmstripStyle.maxHeight = `${_topPanelMaxHeight}px`;
filmstripStyle.zIndex = 1;
if (!_topPanelVisible) {
filmstripStyle.top = `-${_topPanelMaxHeight}px`;
}
}
if (_mainFilmstripVisible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
} else if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
if (_mainFilmstripVisible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
if (_topPanelVisible) {
filmstripStyle.maxHeight = `calc(100% - ${_topPanelMaxHeight}px)`;
}
filmstripStyle.bottom = 0;
filmstripStyle.top = 'auto';
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && !_stageFilmstrip)) {
|| (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.MAIN)) {
filmstripStyle.maxWidth = _verticalViewMaxWidth;
if (!_visible) {
if (!_mainFilmstripVisible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
}
@ -333,14 +389,17 @@ class Filmstrip extends PureComponent <Props, State> {
let toolbar = null;
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
&& _currentLayout !== LAYOUTS.TILE_VIEW && !_stageFilmstrip) {
&& _currentLayout !== LAYOUTS.TILE_VIEW && (filmstripType === FILMSTRIP_TYPE.MAIN
|| (filmstripType === FILMSTRIP_TYPE.STAGE && _topPanelFilmstrip))) {
toolbar = this._renderToggleButton();
}
const filmstrip = (<>
<div
className = { clsx(this.props._videosClassName,
!tileViewActive && !_stageFilmstrip && !_resizableFilmstrip && 'filmstrip-hover',
!tileViewActive && (filmstripType === FILMSTRIP_TYPE.MAIN
|| (filmstripType === FILMSTRIP_TYPE.STAGE && _topPanelFilmstrip))
&& !_resizableFilmstrip && 'filmstrip-hover',
_verticalViewGrid && 'vertical-view-grid') }
id = 'remoteVideos'>
{!_disableSelfView && !_verticalViewGrid && (
@ -348,8 +407,10 @@ class Filmstrip extends PureComponent <Props, State> {
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
{
!tileViewActive && !_stageFilmstrip && <div id = 'filmstripLocalVideoThumbnail'>
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN
&& <div id = 'filmstripLocalVideoThumbnail'>
<Thumbnail
filmstripType = { FILMSTRIP_TYPE.MAIN }
key = 'local' />
</div>
}
@ -361,10 +422,9 @@ class Filmstrip extends PureComponent <Props, State> {
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && !_stageFilmstrip && <Thumbnail
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && <Thumbnail
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
}
</div>
</div>
@ -385,11 +445,14 @@ class Filmstrip extends PureComponent <Props, State> {
style = { filmstripStyle }>
{ toolbar }
{_resizableFilmstrip
? <div className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer) }>
? <div
className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer,
_topPanelFilmstrip && 'top-panel-filmstrip') }>
<div
className = { clsx('dragHandleContainer',
classes.dragHandleContainer,
isMouseDown && 'visible')
isMouseDown && 'visible',
_topPanelFilmstrip && 'top-panel')
}
onMouseDown = { this._onDragHandleMouseDown }>
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
@ -412,10 +475,13 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_onDragHandleMouseDown(e) {
const { _topPanelFilmstrip, _topPanelHeight, _verticalFilmstripWidth } = this.props;
this.setState({
isMouseDown: true,
mousePosition: e.clientX,
dragFilmstripWidth: this.props._verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH
mousePosition: _topPanelFilmstrip ? e.clientY : e.clientX,
dragFilmstripWidth: _verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH,
dragFilmstripHeight: _topPanelHeight || TOP_FILMSTRIP_HEIGHT
});
this.props.dispatch(setUserIsResizing(true));
}
@ -446,16 +512,36 @@ class Filmstrip extends PureComponent <Props, State> {
*/
_onFilmstripResize(e) {
if (this.state.isMouseDown) {
const { dispatch, _verticalFilmstripWidth, _maxFilmstripWidth } = this.props;
const { dragFilmstripWidth, mousePosition } = this.state;
const diff = mousePosition - e.clientX;
const width = Math.max(
Math.min(dragFilmstripWidth + diff, _maxFilmstripWidth),
DEFAULT_FILMSTRIP_WIDTH
);
const {
dispatch,
_verticalFilmstripWidth,
_maxFilmstripWidth,
_topPanelHeight,
_maxTopPanelHeight,
_topPanelFilmstrip
} = this.props;
const { dragFilmstripWidth, dragFilmstripHeight, mousePosition } = this.state;
if (width !== _verticalFilmstripWidth) {
dispatch(setUserFilmstripWidth(width));
if (_topPanelFilmstrip) {
const diff = e.clientY - mousePosition;
const height = Math.max(
Math.min(dragFilmstripHeight + diff, _maxTopPanelHeight),
TOP_FILMSTRIP_HEIGHT
);
if (height !== _topPanelHeight) {
dispatch(setUserFilmstripHeight(height));
}
} else {
const diff = mousePosition - e.clientX;
const width = Math.max(
Math.min(dragFilmstripWidth + diff, _maxFilmstripWidth),
DEFAULT_FILMSTRIP_WIDTH
);
if (width !== _verticalFilmstripWidth) {
dispatch(setUserFilmstripWidth(width));
}
}
}
}
@ -495,7 +581,7 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_onTabIn() {
if (!this.props._isToolboxVisible && this.props._visible) {
if (!this.props._isToolboxVisible && this.props._mainFilmstripVisible) {
this.props.dispatch(showToolbox());
}
}
@ -605,10 +691,10 @@ class Filmstrip extends PureComponent <Props, State> {
_remoteParticipantsLength,
_resizableFilmstrip,
_rows,
_stageFilmstrip,
_thumbnailHeight,
_thumbnailWidth,
_verticalViewGrid
_verticalViewGrid,
filmstripType
} = this.props;
if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
@ -617,7 +703,7 @@ class Filmstrip extends PureComponent <Props, State> {
return null;
}
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || _stageFilmstrip) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || filmstripType !== FILMSTRIP_TYPE.MAIN) {
return (
<FixedSizeGrid
className = 'filmstrip__videos remote-videos'
@ -626,7 +712,7 @@ class Filmstrip extends PureComponent <Props, State> {
height = { _filmstripHeight }
initialScrollLeft = { 0 }
initialScrollTop = { 0 }
itemData = {{ stageFilmstrip: _stageFilmstrip }}
itemData = {{ filmstripType }}
itemKey = { this._gridItemKey }
onItemsRendered = { this._onGridItemsRendered }
overscanRowCount = { 1 }
@ -694,7 +780,11 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_doToggleFilmstrip() {
this.props.dispatch(setFilmstripVisible(!this.props._visible));
const { dispatch, _mainFilmstripVisible, _topPanelFilmstrip, _topPanelVisible } = this.props;
_topPanelFilmstrip
? dispatch(setTopPanelVisible(!_topPanelVisible))
: dispatch(setFilmstripVisible(!_mainFilmstripVisible));
}
_onShortcutToggleFilmstrip: () => void;
@ -710,7 +800,7 @@ class Filmstrip extends PureComponent <Props, State> {
sendAnalytics(createShortcutEvent(
'toggle.filmstrip',
{
enable: this.props._visible
enable: this.props._mainFilmstripVisible
}));
this._doToggleFilmstrip();
@ -729,7 +819,7 @@ class Filmstrip extends PureComponent <Props, State> {
sendAnalytics(createToolbarEvent(
'toggle.filmstrip.button',
{
enable: this.props._visible
enable: this.props._mainFilmstripVisible
}));
this._doToggleFilmstrip();
@ -758,8 +848,15 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {ReactElement}
*/
_renderToggleButton() {
const icon = this.props._visible ? IconMenuDown : IconMenuUp;
const { t, classes, _isVerticalFilmstrip } = this.props;
const {
t,
classes,
_isVerticalFilmstrip,
_mainFilmstripVisible,
_topPanelFilmstrip,
_topPanelVisible
} = this.props;
const icon = (_topPanelFilmstrip ? _topPanelVisible : _mainFilmstripVisible) ? IconMenuDown : IconMenuUp;
const actions = isMobileBrowser()
? { onTouchStart: this._onToggleButtonTouch }
: { onClick: this._onToolbarToggleFilmstrip };
@ -768,9 +865,11 @@ class Filmstrip extends PureComponent <Props, State> {
<div
className = { clsx(classes.toggleFilmstripContainer,
_isVerticalFilmstrip && classes.toggleVerticalFilmstripContainer,
_topPanelFilmstrip && classes.toggleTopPanelContainer,
_topPanelFilmstrip && !_topPanelVisible && classes.toggleTopPanelContainerHidden,
'toggleFilmstripContainer') }>
<button
aria-expanded = { this.props._visible }
aria-expanded = { this.props._mainFilmstripVisible }
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
className = { classes.toggleFilmstripButton }
id = 'toggleFilmstripButton'
@ -795,32 +894,38 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { _hasScroll = false } = ownProps;
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
const toolbarButtons = getToolbarButtons(state);
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const disableSelfView = shouldHideSelfView(state);
const { clientWidth } = state['features/base/responsive-ui'];
const { clientWidth, clientHeight } = state['features/base/responsive-ui'];
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && isMobileBrowser();
const _topPanelVisible = isStageFilmstripTopPanel(state) && topPanelVisible;
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
let isVisible = visible || filmstripType !== FILMSTRIP_TYPE.MAIN;
if (_topPanelFilmstrip) {
isVisible = _topPanelVisible;
}
const videosClassName = `filmstrip__videos${isVisible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${isVisible ? '' : 'hidden'}`.trim();
const _currentLayout = getCurrentLayout(state);
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (!ownProps._stageFilmstrip && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|| (filmstripType === FILMSTRIP_TYPE.MAIN && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
return {
_className: className,
@ -833,8 +938,14 @@ function _mapStateToProps(state, ownProps) {
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_mainFilmstripVisible: visible,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
_remoteParticipantsLength: _remoteParticipants.length,
_thumbnailsReordered: enableThumbnailReordering,
_topPanelHeight: topPanelHeight.current,
_topPanelMaxHeight: topPanelHeight.current || TOP_FILMSTRIP_HEIGHT,
_topPanelVisible,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_videosClassName: videosClassName

View File

@ -9,6 +9,7 @@ import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
FILMSTRIP_TYPE,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE } from '../../constants';
import { isFilmstripResizable, showGridInVerticalView } from '../../functions.web';
@ -85,15 +86,16 @@ type Props = {
/**
* 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
_videosClassName: string
};
const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
const MainFilmstrip = (props: Props) => (
<span>
<Filmstrip
{ ...props }
filmstripType = { FILMSTRIP_TYPE.MAIN } />
</span>
);
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
@ -104,7 +106,7 @@ const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
@ -189,13 +191,11 @@ function _mapStateToProps(state) {
_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
};

View File

@ -0,0 +1,125 @@
// @flow
import React from 'react';
import { connect } from '../../../base/redux';
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
FILMSTRIP_TYPE
} from '../../constants';
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
};
const ScreenshareFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW
&& props._remoteParticipantsLength === 1 && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
filmstripType = { FILMSTRIP_TYPE.SCREENSHARE } />
</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 {
filmstripHeight,
filmstripWidth,
thumbnailSize
} = state['features/filmstrip'].screenshareFilmstripDimensions;
const screenshares = state['features/video-layout'].remoteScreenShares;
return {
_columns: 1,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: filmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipants: screenshares.length ? [ screenshares[0] ] : [],
_resizableFilmstrip: false,
_rows: 1,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_verticalViewGrid: false,
_verticalViewBackground: false
};
}
export default connect(_mapStateToProps)(ScreenshareFilmstrip);

View File

@ -8,9 +8,11 @@ import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_TYPE,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { getActiveParticipantsIds } from '../../functions';
import { isFilmstripResizable, isStageFilmstripTopPanel } from '../../functions.web';
import Filmstrip from './Filmstrip';
@ -84,19 +86,14 @@ type Props = {
/**
* 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
_videosClassName: string
};
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
_stageFilmstrip = { true } />
filmstripType = { FILMSTRIP_TYPE.STAGE } />
</span>
);
@ -109,7 +106,6 @@ const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_
*/
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 {
@ -139,19 +135,19 @@ function _mapStateToProps(state) {
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
const _topPanelFilmstrip = isStageFilmstripTopPanel(state);
return {
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipantsLength: activeParticipants.length,
_remoteParticipants: activeParticipants,
_resizableFilmstrip: false,
_resizableFilmstrip: isFilmstripResizable(state) && _topPanelFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_visible: visible,
_topPanelFilmstrip,
_verticalViewGrid: false,
_verticalViewBackground: false
};

View File

@ -37,6 +37,7 @@ import { togglePinStageParticipant } from '../../actions';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
FILMSTRIP_TYPE,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER,
THUMBNAIL_TYPE,
VIDEO_TEST_EVENTS
@ -234,6 +235,11 @@ export type Props = {|
*/
dispatch: Function,
/**
* The type of filmstrip the tile is displayed in.
*/
filmstripType: string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
*/
@ -244,11 +250,6 @@ 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.
*/
@ -993,7 +994,7 @@ class Thumbnail extends Component<Props, State> {
_thumbnailType,
_videoTrack,
classes,
stageFilmstrip
filmstripType
} = this.props;
const { id } = _participant || {};
const { isHovered, popoverVisible } = this.state;
@ -1031,8 +1032,8 @@ class Thumbnail extends Component<Props, State> {
<span
className = { containerClassName }
id = { local
? `localVideoContainer${stageFilmstrip ? '_stage' : ''}`
: `participant_${id}${stageFilmstrip ? '_stage' : ''}`
? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
: `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
}
{ ...(_isMobile
? {
@ -1168,7 +1169,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID, stageFilmstrip } = ownProps;
const { participantID, filmstripType = FILMSTRIP_TYPE.MAIN } = ownProps;
const participant = getParticipantByIdOrUndefined(state, participantID);
const id = participant?.id;
@ -1199,7 +1200,7 @@ function _mapStateToProps(state, ownProps): Object {
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
const activeParticipants = getActiveParticipantsIds(state);
const tileType = getThumbnailTypeFromLayout(_currentLayout, stageFilmstrip);
const tileType = getThumbnailTypeFromLayout(_currentLayout, filmstripType);
switch (tileType) {
case THUMBNAIL_TYPE.VERTICAL:
@ -1244,7 +1245,8 @@ function _mapStateToProps(state, ownProps): Object {
const {
stageFilmstripDimensions = {
thumbnailSize: {}
}
},
screenshareFilmstripDimensions
} = state['features/filmstrip'];
size = {
@ -1252,9 +1254,16 @@ function _mapStateToProps(state, ownProps): Object {
_height: thumbnailSize?.height
};
if (stageFilmstrip) {
if (filmstripType === FILMSTRIP_TYPE.STAGE) {
const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
size = {
_width,
_height
};
} else if (filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
const { width: _width, height: _height } = screenshareFilmstripDimensions.thumbnailSize;
size = {
_width,
_height

View File

@ -7,7 +7,7 @@ import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN } from '../../constants';
import { TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN, FILMSTRIP_TYPE } from '../../constants';
import { showGridInVerticalView, getActiveParticipantsIds } from '../../functions';
import Thumbnail from './Thumbnail';
@ -22,6 +22,11 @@ type Props = {
*/
_disableSelfView: boolean,
/**
* The type of filmstrip this thumbnail is displayed in.
*/
_filmstripType: string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
*/
@ -37,11 +42,6 @@ type Props = {
*/
_isLocalScreenShare: boolean,
/**
* Whether or not the filmstrip is used a stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The width of the thumbnail. Used for expanding the width of the thumbnails on last row in case
* there is empty space.
@ -97,10 +97,10 @@ class ThumbnailWrapper extends Component<Props> {
render() {
const {
_disableSelfView,
_filmstripType = FILMSTRIP_TYPE.MAIN,
_isLocalScreenShare = false,
_horizontalOffset = 0,
_participantID,
_stageFilmstrip,
_thumbnailWidth,
style
} = this.props;
@ -112,9 +112,9 @@ class ThumbnailWrapper extends Component<Props> {
if (_participantID === 'local') {
return _disableSelfView ? null : (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = 'local'
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
@ -122,20 +122,20 @@ class ThumbnailWrapper extends Component<Props> {
if (_isLocalScreenShare) {
return _disableSelfView ? null : (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = 'localScreenShare'
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
return (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
@ -158,7 +158,8 @@ function _mapStateToProps(state, ownProps) {
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _verticalViewGrid = showGridInVerticalView(state);
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const filmstripType = ownProps.data?.filmstripType;
const stageFilmstrip = filmstripType === FILMSTRIP_TYPE.STAGE;
const sortedActiveParticipants = activeParticipants.sort();
const remoteParticipants = stageFilmstrip ? sortedActiveParticipants : remote;
const remoteParticipantsLength = remoteParticipants.length;
@ -235,9 +236,9 @@ function _mapStateToProps(state, ownProps) {
if (stageFilmstrip) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_participantID: remoteParticipants[index] === localId ? 'local' : remoteParticipants[index],
_horizontalOffset: horizontalOffset,
_stageFilmstrip: stageFilmstrip,
_thumbnailWidth: thumbnailWidth
};
}
@ -260,6 +261,7 @@ function _mapStateToProps(state, ownProps) {
if (!iAmRecorder && index === localIndex) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_participantID: 'local',
_horizontalOffset: horizontalOffset,
_thumbnailWidth: thumbnailWidth
@ -269,6 +271,7 @@ function _mapStateToProps(state, ownProps) {
if (sourceNameSignalingEnabled && !iAmRecorder && localScreenShare && index === localScreenShareIndex) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_isLocalScreenShare: true,
_participantID: localScreenShare?.id,
_horizontalOffset: horizontalOffset,
@ -277,12 +280,22 @@ function _mapStateToProps(state, ownProps) {
}
return {
_filmstripType: filmstripType,
_participantID: remoteParticipants[remoteIndex],
_horizontalOffset: horizontalOffset,
_thumbnailWidth: thumbnailWidth
};
}
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
const { remoteScreenShares } = state['features/video-layout'];
return {
_filmstripType: filmstripType,
_participantID: remoteScreenShares[remoteScreenShares.length - 1]
};
}
const { index } = ownProps;
if (typeof index !== 'number' || remoteParticipantsLength <= index) {

View File

@ -5,6 +5,7 @@ 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 ScreenshareFilmstrip } from './ScreenshareFilmstrip';
export { default as StageFilmstrip } from './StageFilmstrip';
export { default as StatusIndicators } from './StatusIndicators';
export { default as Thumbnail } from './Thumbnail';

View File

@ -23,6 +23,7 @@ export const styles = theme => {
left: 'calc(50% - 16px)',
opacity: 0,
transition: 'opacity .3s',
zIndex: 1,
'&:hover': {
backgroundColor: theme.palette.ui02
@ -53,8 +54,18 @@ export const styles = theme => {
top: 'calc(50% - 12px)'
},
toggleTopPanelContainer: {
transform: 'rotate(180deg)',
bottom: 'calc(-24px - 6px)',
top: 'auto'
},
toggleTopPanelContainerHidden: {
visibility: 'hidden'
},
filmstrip: {
transition: 'background .2s ease-in-out, right 1s, bottom 1s, height .3s ease-in',
transition: 'background .2s ease-in-out, right 1s, bottom 1s, top 1s, height .3s ease-in',
right: 0,
bottom: 0,
@ -111,6 +122,10 @@ export const styles = theme => {
'& .avatar-container': {
maxWidth: 'initial',
maxHeight: 'initial'
},
'&.top-panel-filmstrip': {
flexDirection: 'column'
}
},
@ -137,6 +152,18 @@ export const styles = theme => {
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
},
'&.top-panel': {
order: 2,
width: '100%',
height: '9px',
cursor: 'row-resize',
'& .dragHandle': {
height: '3px',
width: '100px'
}
}
},

View File

@ -281,6 +281,12 @@ export const FILMSTRIP_GRID_BREAKPOINT = 300;
*/
export const FILMSTRIP_BREAKPOINT_OFFSET = 5;
/**
* The minimum height for the stage view
* (used to determine the maximum height of the user-resizable top panel).
*/
export const MIN_STAGE_VIEW_HEIGHT = 700;
/**
* The minimum width for the stage view
* (used to determine the maximum width of the user-resizable vertical filmstrip).
@ -298,7 +304,21 @@ export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL
*/
export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
/**
* The types of filmstrip.
*/
export const FILMSTRIP_TYPE = {
MAIN: 'main',
STAGE: 'stage',
SCREENSHARE: 'screenshare'
};
/**
* The max number of participants to be displayed on the stage filmstrip.
*/
export const MAX_ACTIVE_PARTICIPANTS = 6;
/**
* Top filmstrip default height.
*/
export const TOP_FILMSTRIP_HEIGHT = 180;

View File

@ -6,6 +6,7 @@ import { MEDIA_TYPE } from '../base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
getParticipantCountWithFake,
getPinnedParticipant
} from '../base/participants';
@ -32,6 +33,7 @@ import {
DISPLAY_AVATAR,
DISPLAY_VIDEO,
FILMSTRIP_GRID_BREAKPOINT,
FILMSTRIP_TYPE,
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
@ -296,7 +298,8 @@ export function calculateResponsiveTileViewDimensions({
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
minTileHeight
}) {
let height, width;
let columns, rows;
@ -324,7 +327,8 @@ export function calculateResponsiveTileViewDimensions({
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: false,
noHorizontalContainerMargin
noHorizontalContainerMargin,
minTileHeight
});
if (size) {
@ -413,10 +417,11 @@ export function calculateThumbnailSizeForTileView({
clientHeight,
disableResponsiveTiles = false,
disableTileEnlargement = false,
noHorizontalContainerMargin = false
noHorizontalContainerMargin = false,
minTileHeight
}: Object) {
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
const minHeight = getThumbnailMinHeight(clientWidth);
const minHeight = minTileHeight || getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
@ -506,6 +511,7 @@ export function getVerticalFilmstripVisibleAreaWidth() {
*/
export function computeDisplayModeFromInput(input: Object) {
const {
filmstripType,
isActiveParticipant,
isAudioOnly,
isCurrentlyOnLargeVideo,
@ -515,7 +521,6 @@ export function computeDisplayModeFromInput(input: Object) {
isRemoteParticipant,
multipleVideoSupport,
stageParticipantsVisible,
stageFilmstrip,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
@ -534,8 +539,8 @@ export function computeDisplayModeFromInput(input: Object) {
}
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant && !stageFilmstrip))) {
if (!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant))) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@ -569,12 +574,13 @@ export function getDisplayModeInput(props: Object, state: Object) {
_participant,
_stageParticipantsVisible,
_videoTrack,
stageFilmstrip
filmstripType = FILMSTRIP_TYPE.MAIN
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived } = state;
return {
filmstripType,
isActiveParticipant: _isActiveParticipant,
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isAudioOnly: _isAudioOnly,
@ -588,7 +594,6 @@ export function getDisplayModeInput(props: Object, state: Object) {
isVirtualScreenshareParticipant: _isVirtualScreenshareParticipant,
multipleVideoSupport: _multipleVideoSupport,
stageParticipantsVisible: _stageParticipantsVisible,
stageFilmstrip,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@ -717,8 +722,24 @@ export function isStageFilmstripAvailable(state, minParticipantCount = 0) {
const { remoteScreenShares } = state['features/video-layout'];
const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
return isStageFilmstripEnabled(state) && remoteScreenShares.length === 0 && !sharedVideo
&& activeParticipants.length >= minParticipantCount;
return isStageFilmstripEnabled(state) && !sharedVideo
&& activeParticipants.length >= minParticipantCount
&& (isTopPanelEnabled(state) || remoteScreenShares.length === 0);
}
/**
* Whether the stage filmstrip should be displayed on the top.
*
* @param {Object} state - Redux state.
* @param {number} minParticipantCount - The min number of participants for the stage filmstrip
* to be displayed.
* @returns {boolean}
*/
export function isStageFilmstripTopPanel(state, minParticipantCount = 0) {
const { remoteScreenShares } = state['features/video-layout'];
return isTopPanelEnabled(state)
&& isStageFilmstripAvailable(state, minParticipantCount) && remoteScreenShares.length > 0;
}
/**
@ -737,10 +758,10 @@ export function isStageFilmstripEnabled(state) {
* Gets the thumbnail type by filmstrip type.
*
* @param {string} currentLayout - Current app layout.
* @param {boolean} isStageFilmstrip - Whether the filmstrip is stage filmstrip or not.
* @param {string} filmstripType - The current filmstrip type.
* @returns {string}
*/
export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = false) {
export function getThumbnailTypeFromLayout(currentLayout, filmstripType) {
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
return THUMBNAIL_TYPE.TILE;
@ -749,10 +770,24 @@ export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = fal
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.HORIZONTAL;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
if (isStageFilmstrip) {
if (filmstripType !== FILMSTRIP_TYPE.MAIN) {
return THUMBNAIL_TYPE.TILE;
}
return THUMBNAIL_TYPE.VERTICAL;
}
}
/**
* Whether or not the top panel is enabled.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isTopPanelEnabled(state) {
const { filmstrip } = state['features/base/config'];
const participantsCount = getParticipantCount(state);
return !filmstrip?.disableTopPanel && participantsCount >= (filmstrip?.minParticipantCountForTopPanel ?? 50);
}

View File

@ -31,6 +31,7 @@ import {
import {
addStageParticipant,
removeStageParticipant,
setFilmstripHeight,
setFilmstripWidth,
setStageParticipants
} from './actions';
@ -38,7 +39,9 @@ import {
ACTIVE_PARTICIPANT_TIMEOUT,
DEFAULT_FILMSTRIP_WIDTH,
MAX_ACTIVE_PARTICIPANTS,
MIN_STAGE_VIEW_WIDTH
MIN_STAGE_VIEW_HEIGHT,
MIN_STAGE_VIEW_WIDTH,
TOP_FILMSTRIP_HEIGHT
} from './constants';
import {
isFilmstripResizable,
@ -77,19 +80,27 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
if (isFilmstripResizable(state)) {
const { width: filmstripWidth } = state['features/filmstrip'];
const { clientWidth } = action;
let width;
const { width: filmstripWidth, topPanelHeight } = state['features/filmstrip'];
const { clientWidth, clientHeight } = action;
let height, width;
if (filmstripWidth.current > clientWidth - MIN_STAGE_VIEW_WIDTH) {
width = Math.max(clientWidth - MIN_STAGE_VIEW_WIDTH, DEFAULT_FILMSTRIP_WIDTH);
} else {
width = Math.min(clientWidth - MIN_STAGE_VIEW_WIDTH, filmstripWidth.userSet);
}
if (width !== filmstripWidth.current) {
store.dispatch(setFilmstripWidth(width));
}
if (topPanelHeight.current > clientHeight - MIN_STAGE_VIEW_HEIGHT) {
height = Math.max(clientHeight - MIN_STAGE_VIEW_HEIGHT, TOP_FILMSTRIP_HEIGHT);
} else {
height = Math.min(clientHeight - MIN_STAGE_VIEW_HEIGHT, topPanelHeight.userSet);
}
if (height !== topPanelHeight.current) {
store.dispatch(setFilmstripHeight(height));
}
}
break;
}

View File

@ -19,7 +19,11 @@ import {
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
CLEAR_STAGE_PARTICIPANTS
CLEAR_STAGE_PARTICIPANTS,
SET_SCREENSHARING_TILE_DIMENSIONS,
SET_USER_FILMSTRIP_HEIGHT,
SET_FILMSTRIP_HEIGHT,
SET_TOP_PANEL_VISIBILITY
} from './actionTypes';
const DEFAULT_STATE = {
@ -76,6 +80,11 @@ const DEFAULT_STATE = {
*/
remoteParticipants: [],
/**
* The dimensions of the screenshare filmstrip.
*/
screenshareFilmstripDimensions: {},
/**
* The stage filmstrip view dimensions.
*
@ -92,6 +101,27 @@ const DEFAULT_STATE = {
*/
tileViewDimensions: {},
/**
* The height of the resizable top panel.
*/
topPanelHeight: {
/**
* Current height. Affected by: user top panel resize,
* window resize.
*/
current: null,
/**
* Height set by user resize. Used as the preferred height.
*/
userSet: null
},
/**
* The indicator determines if the top panel is visible.
*/
topPanelVisible: true,
/**
* The vertical view dimensions.
*
@ -227,6 +257,15 @@ ReducerRegistry.register(
...state
};
}
case SET_FILMSTRIP_HEIGHT:{
return {
...state,
topPanelHeight: {
...state.topPanelHeight,
current: action.height
}
};
}
case SET_FILMSTRIP_WIDTH: {
return {
...state,
@ -236,6 +275,17 @@ ReducerRegistry.register(
}
};
}
case SET_USER_FILMSTRIP_HEIGHT: {
const { height } = action;
return {
...state,
topPanelHeight: {
current: height,
userSet: height
}
};
}
case SET_USER_FILMSTRIP_WIDTH: {
const { width } = action;
@ -283,6 +333,18 @@ ReducerRegistry.register(
activeParticipants: []
};
}
case SET_SCREENSHARING_TILE_DIMENSIONS: {
return {
...state,
screenshareFilmstripDimensions: action.dimensions
};
}
case SET_TOP_PANEL_VISIBILITY: {
return {
...state,
topPanelVisible: action.visible
};
}
}
return state;

View File

@ -14,6 +14,7 @@ import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layou
import {
clearStageParticipants,
setHorizontalViewDimensions,
setScreensharingTileDimensions,
setStageFilmstripViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
@ -23,7 +24,8 @@ import {
DISPLAY_DRAWER_THRESHOLD
} from './constants';
import {
isFilmstripResizable
isFilmstripResizable,
isTopPanelEnabled
} from './functions';
import './subscriber.any';
@ -176,7 +178,8 @@ StateListenerRegistry.register(
visible: state['features/filmstrip'].visible,
clientWidth: state['features/base/responsive-ui'].clientWidth,
clientHeight: state['features/base/responsive-ui'].clientHeight,
tileView: state['features/video-layout'].tileViewEnabled
tileView: state['features/video-layout'].tileViewEnabled,
height: state['features/filmstrip'].topPanelHeight?.current
};
},
/* listener */(_, store) => {
@ -198,3 +201,27 @@ StateListenerRegistry.register(
store.dispatch(selectParticipantInLargeVideo());
}
});
/**
* Listens for changes to determine the size of the screenshare filmstrip.
*/
StateListenerRegistry.register(
/* selector */ state => {
return {
length: state['features/video-layout'].remoteScreenShares.length,
clientWidth: state['features/base/responsive-ui'].clientWidth,
clientHeight: state['features/base/responsive-ui'].clientHeight,
height: state['features/filmstrip'].topPanelHeight?.current,
width: state['features/filmstrip'].width?.current,
visible: state['features/filmstrip'].visible,
topPanelVisible: state['features/filmstrip'].topPanelVisible
};
},
/* listener */({ length }, store) => {
if (length >= 1 && isTopPanelEnabled(store.getState())) {
store.dispatch(setScreensharingTileDimensions());
}
}, {
deepEquals: true
});