feat(Filmstrip): Pagination.

This commit is contained in:
Hristo Terezov 2021-03-26 15:23:05 -05:00
parent 64ae9c7953
commit 16cfda3c7a
19 changed files with 957 additions and 472 deletions

View File

@ -33,18 +33,18 @@
}
&__videos {
@extend %align-right;
position:relative;
padding: 0;
/* The filmstrip should not be covered by the left toolbar. */
bottom: 0;
width:auto;
overflow: visible !important;
&#remoteVideos {
border: $thumbnailsBorder solid transparent;
transition: bottom 2s;
flex-grow: 1;
display: flex;
flex-direction: row-reverse;
@include minHWAutoFix()
}
@ -60,41 +60,25 @@
&.hidden {
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
}
.remote-videos-container {
display: flex;
}
}
.remote-videos-container {
transition: opacity 1s;
.remote-videos {
& > div {
transition: opacity 1s;
position: absolute;
}
&.is-not-overflowing > div {
right: 2px;
}
}
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
}
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
width: auto;
justify-content: flex-end;
flex-direction: row;
#filmstripRemoteVideosContainer {
flex-direction: row-reverse;
/**
* Add padding as a hack for Firefox not to show scrollbars when
* unnecessary.
*/
padding: 1px 0;
overflow-y: hidden;
overflow-x: scroll;
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
}
}
}
@ -103,25 +87,3 @@
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: row-reverse.
*/
@mixin undoRowReverseVideos() {
.horizontal-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: row;
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoRowReverseVideos();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoRowReverseVideos();
}

View File

@ -10,13 +10,11 @@
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
}
#filmstripRemoteVideos {
.remote-videos {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.filmstrip__videos .videocontainer {
@ -34,6 +32,9 @@
*/
height: 100% !important;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.filmstrip {
@ -50,6 +51,10 @@
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
.remote-videos{
width: calc(100vw - #{$sidebarWidth});
}
}
}
}
@ -62,63 +67,49 @@
display: block;
}
#filmstripRemoteVideos {
.remote-videos {
box-sizing: border-box;
/**
* Allow vertical scrolling of the thumbnails.
*/
overflow-x: hidden;
overflow-y: auto;
}
/**
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
#filmstripRemoteVideosContainer {
align-content: center;
align-items: center;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
margin-top: auto;
margin-bottom: auto;
justify-content: center;
.videocontainer {
border: 0;
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
& > div {
align-content: center;
align-items: center;
box-sizing: border-box;
display: block;
margin: 2px;
}
display: flex;
margin-top: auto;
margin-bottom: auto;
justify-content: center;
position: absolute;
video {
object-fit: contain;
}
.videocontainer {
border: 0;
box-sizing: border-box;
display: block;
margin: 2px;
}
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
*/
@media only screen and (max-width: 500px) {
video {
object-fit: cover;
object-fit: contain;
}
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
*/
@media only screen and (max-width: 500px) {
video {
object-fit: cover;
}
}
}
}
.has-overflow#filmstripRemoteVideosContainer {
align-content: baseline;
}
.has-overflow .videocontainer {
align-self: baseline;
}
}
.shift-right #filmstripRemoteVideosContainer {
.shift-right .remote-videos > div {
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
* from which we subtract the chat size.

View File

@ -1,8 +1,10 @@
.vertical-filmstrip .filmstrip {
&.hide-videos {
.remote-videos-container {
opacity: 0;
pointer-events: none;
.remote-videos {
& > div {
opacity: 0;
pointer-events: none;
}
}
}
@ -39,10 +41,6 @@
right: 0;
z-index: $filmstripVideosZ;
&.reduce-height {
height: calc(100% - #{$newToolbarSizeWithPadding});
}
/**
* Hide videos by making them slight to the right.
*/
@ -98,33 +96,10 @@
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripRemoteVideos {
.remote-videos {
padding: 0;
}
#filmstripRemoteVideos {
@include minHWAutoFix();
display: flex;
flex: 1;
flex-direction: column-reverse;
height: auto;
overflow-x: hidden;
overflow-y: scroll;
#filmstripRemoteVideosContainer {
@include minHWAutoFix();
flex-direction: column-reverse;
overflow: visible;
width: calc(100% - 8px); // 8px for margin + border of the thumbnails
.videocontainer {
height: 0px;
width: 100%;
}
}
}
#remoteVideos {
@include minHWAutoFix();
@ -132,56 +107,21 @@
flex-grow: 1;
}
.remote-videos-container {
&.reduce-height {
height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
}
.remote-videos {
display: flex;
transition: opacity 1s;
}
transition: height .3s ease-in;
.hide-scrollbar#filmstripRemoteVideos {
margin-right: 7px; // Scrollbar size
&::-webkit-scrollbar {
display: none;
& > div {
position: absolute;
transition: opacity 1s;
}
&.is-not-overflowing > div {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should
* start scrolling from the bottom of the filmstrip, but in those browsers the
* scrolling won't happen. Per W3C spec, scrolling should happen from the
* bottom. As such, use css hacks to get around the css issue, with the intent
* being to remove the hacks as the spec is supported.
*/
@mixin undoColumnReverseVideos() {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
flex-direction: column;
}
}
}
/**
* FF does not include the scroll width when calculating the size of the content. That's why we need to include
* ourselves the width of the scroll so that the remote videos are aligned with the local one.
*/
@mixin filmstripSizeWithoutScroll {
.vertical-filmstrip {
#remoteVideos #filmstripRemoteVideos {
#filmstripRemoteVideosContainer {
width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
}
}
}
}
/** Firefox detection hack **/
@-moz-document url-prefix() {
@include undoColumnReverseVideos();
@include filmstripSizeWithoutScroll();
}
/** Edge detection hack **/
@supports (-ms-ime-align:auto) {
@include undoColumnReverseVideos();
}

14
package-lock.json generated
View File

@ -11390,6 +11390,11 @@
}
}
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -15318,6 +15323,15 @@
}
}
},
"react-window": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
"integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
"requires": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
}
},
"react-youtube": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",

View File

@ -94,6 +94,7 @@
"react-textarea-autosize": "8.3.0",
"react-transition-group": "2.4.0",
"react-youtube": "7.13.1",
"react-window": "1.8.6",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",

View File

@ -3,6 +3,7 @@
import React, { Component } from 'react';
import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
import { connect } from '../../../redux';
import logger from '../../logger';
/**
@ -10,6 +11,16 @@ import logger from '../../logger';
*/
type Props = {
/**
* Represents muted property of the underlying audio element.
*/
_muted: ?Boolean,
/**
* Represents volume property of the underlying audio element.
*/
_volume: ?number,
/**
* The value of the id attribute of the audio element.
*/
@ -28,26 +39,15 @@ type Props = {
autoPlay: boolean,
/**
* Represents muted property of the underlying audio element.
* The ID of the participant associated with the audio element.
*/
muted: ?Boolean,
/**
* Represents volume property of the underlying audio element.
*/
volume: ?number,
/**
* A function that will be executed when the reference to the underlying audio element changes in order to report
* the initial volume value.
*/
onInitialVolumeSet: Function
participantId: string
};
/**
* The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
*/
export default class AudioTrack extends Component<Props> {
class AudioTrack extends Component<Props> {
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
@ -94,14 +94,14 @@ export default class AudioTrack extends Component<Props> {
this._attachTrack(this.props.audioTrack);
if (this._ref) {
const { muted, volume } = this.props;
const { _muted, _volume } = this.props;
if (typeof volume === 'number') {
this._ref.volume = volume;
if (typeof _volume === 'number') {
this._ref.volume = _volume;
}
if (typeof muted === 'boolean') {
this._ref.muted = muted;
if (typeof _muted === 'boolean') {
this._ref.muted = _muted;
}
}
}
@ -136,14 +136,14 @@ export default class AudioTrack extends Component<Props> {
if (this._ref) {
const currentVolume = this._ref.volume;
const nextVolume = nextProps.volume;
const nextVolume = nextProps._volume;
if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
this._ref.volume = nextVolume;
}
const currentMuted = this._ref.muted;
const nextMuted = nextProps.muted;
const nextMuted = nextProps._muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
this._ref.muted = nextMuted;
@ -258,10 +258,24 @@ export default class AudioTrack extends Component<Props> {
*/
_setRef(audioElement: ?HTMLAudioElement) {
this._ref = audioElement;
const { onInitialVolumeSet } = this.props;
if (this._ref && onInitialVolumeSet) {
onInitialVolumeSet(this._ref.volume);
}
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AudioTrack}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The props passed to the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantsVolume } = state['features/filmstrip'];
return {
_muted: state['features/base/config'].startSilent,
_volume: participantsVolume[ownProps.participantId]
};
}
export default connect(_mapStateToProps)(AudioTrack);

View File

@ -1,3 +1,4 @@
export { default as Audio } from './Audio';
export { default as AudioTrack } from './AudioTrack';
export { default as Video } from './Video';
export { default as VideoTrack } from './VideoTrack';

View File

@ -27,7 +27,7 @@ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE';
* gridDimensions: {
* columns: number,
* height: number,
* visibleRows: number,
* minVisibleRows: number,
* width: number
* },
* thumbnailSize: {
@ -49,3 +49,24 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
* }
*/
export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
/**
* The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
*
* {
* type: SET_VERTICAL_VIEW_DIMENSIONS,
* dimensions: Object
* }
*/
export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
/**
* The type of (redux) action which sets the volume for a thumnail's audio.
*
* {
* type: SET_VOLUME,
* participantId: string,
* volume: number
* }
*/
export const SET_VOLUME = 'SET_VOLUME';

View File

@ -1,64 +1,120 @@
// @flow
import type { Dispatch } from 'redux';
import { pinParticipant } from '../base/participants';
import { toState } from '../base/redux';
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
/**
* The size of the side margins for the entire tile view area.
*/
const TILE_VIEW_SIDE_MARGINS = 20;
import {
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
calculateThumbnailSizeForHorizontalView,
calculateThumbnailSizeForTileView,
calculateThumbnailSizeForVerticalView
} from './functions';
/**
* Sets the dimensions of the tile view grid.
*
* @param {Object} dimensions - Whether the filmstrip is visible.
* @param {Object} windowSize - The size of the window.
* @param {Object | Function} stateful - An object or function that can be
* resolved to Redux state using the {@code toState} function.
* @returns {{
* type: SET_TILE_VIEW_DIMENSIONS,
* dimensions: Object
* }}
* @returns {Function}
*/
export function setTileViewDimensions(dimensions: Object, windowSize: Object, stateful: Object | Function) {
const state = toState(stateful);
const { clientWidth, clientHeight } = windowSize;
const { disableResponsiveTiles } = state['features/base/config'];
export function setTileViewDimensions(dimensions: Object) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableResponsiveTiles } = state['features/base/config'];
const {
height,
width
} = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth,
clientHeight,
disableResponsiveTiles
});
const { columns, rows } = dimensions;
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
const thumbnailSize = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth,
clientHeight,
disableResponsiveTiles
});
const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
gridDimensions: dimensions,
thumbnailSize: {
height,
width
},
filmstripHeight,
filmstripWidth
}
});
};
}
return {
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
gridDimensions: dimensions,
thumbnailSize,
filmstripWidth
}
/**
* Sets the dimensions of the thumbnails in vertical view.
*
* @returns {Function}
*/
export function setVerticalViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
dispatch({
type: SET_VERTICAL_VIEW_DIMENSIONS,
dimensions: {
...thumbnails,
remoteVideosContainer: {
width: thumbnails?.local?.width
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
height: clientHeight - thumbnails?.local?.height - VERTICAL_FILMSTRIP_VERTICAL_MARGIN
}
}
});
};
}
/**
* Sets the dimensions of the thumbnails in horizontal view.
*
* @param {number} clientHeight - The height of the window.
* @returns {{
* type: SET_HORIZONTAL_VIEW_DIMENSIONS,
* dimensions: Object
* }}
* @returns {Function}
*/
export function setHorizontalViewDimensions(clientHeight: number = 0) {
return {
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
dimensions: calculateThumbnailSizeForHorizontalView(clientHeight)
export function setHorizontalViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
dispatch({
type: SET_HORIZONTAL_VIEW_DIMENSIONS,
dimensions: {
...thumbnails,
remoteVideosContainer: {
width: clientWidth - thumbnails?.local?.width - HORIZONTAL_FILMSTRIP_MARGIN,
height: thumbnails?.local?.height
+ TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
}
}
});
};
}
@ -78,4 +134,23 @@ export function clickOnVideo(n: number) {
};
}
/**
* Sets the volume for a thumnail's audio.
*
* @param {string} participantId - The participant ID asociated with the audio.
* @param {string} volume - The volume level.
* @returns {{
* type: SET_VOLUME,
* participantId: string,
* volume: number
* }}
*/
export function setVolume(participantId: string, volume: number) {
return {
type: SET_VOLUME,
participantId,
volume
};
}
export * from './actions.native';

View File

@ -0,0 +1,65 @@
/* @flow */
import React from 'react';
import { AudioTrack, MEDIA_TYPE } from '../../../base/media';
import { connect } from '../../../base/redux';
/**
* The type of the React {@code Component} props of {@link AudioTracksContainer}.
*/
type Props = {
/**
* All media tracks stored in redux.
*/
_tracks: Array<Object>
};
/**
* A container for the remote tracks audio elements.
*
* @param {Props} props - The props of the component.
* @returns {Array<ReactElement>}
*/
function AudioTracksContainer(props: Props) {
const { _tracks } = props;
const remoteAudioTracks = _tracks.filter(t => !t.local && t.mediaType === MEDIA_TYPE.AUDIO);
return (
<div>
{
remoteAudioTracks.map(t => {
const { jitsiTrack, participantId } = t;
const audioTrackId = jitsiTrack && jitsiTrack.getId();
const id = `remoteAudio_${audioTrackId || ''}`;
return (
<AudioTrack
audioTrack = { t }
id = { id }
key = { id }
participantId = { participantId } />);
})
}
</div>);
}
/**
* Maps (parts of) the Redux state to the associated {@code AudioTracksContainer}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
// NOTE: The disadvantage of this approach is that the component will re-render on any track change.
// One way to solve the problem would be to pass only the participant ID to the AudioTrack component and
// find the corresponding track inside the AudioTrack's mapStateToProps. But currently this will be very
// inefficient because features/base/tracks is an array and in order to find a track by participant ID
// we need to go trough the array. Introducing a map participantID -> track could be beneficial in this case.
return {
_tracks: state['features/base/tracks']
};
}
export default connect(_mapStateToProps)(AudioTracksContainer);

View File

@ -1,6 +1,7 @@
/* @flow */
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { FixedSizeList, FixedSizeGrid } from 'react-window';
import type { Dispatch } from 'redux';
import {
@ -11,15 +12,17 @@ import {
import { getToolbarButtons } from '../../../base/config';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible } from '../../actions';
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
import { shouldRemoteVideosBeVisible } from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
import Thumbnail from './Thumbnail';
import ThumbnailWrapper from './ThumbnailWrapper';
declare var APP: Object;
declare var interfaceConfig: Object;
@ -50,14 +53,9 @@ type Props = {
_filmstripWidth: number,
/**
* Whether the filmstrip scrollbar should be hidden or not.
* The height of the filmstrip.
*/
_hideScrollbar: boolean,
/**
* Whether the filmstrip toolbar should be hidden or not.
*/
_hideToolbar: boolean,
_filmstripHeight: number,
/**
* Whether the filmstrip button is enabled.
@ -67,13 +65,29 @@ type Props = {
/**
* The participants in the call.
*/
_participants: Array<Object>,
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
@ -106,7 +120,7 @@ type Props = {
*
* @extends Component
*/
class Filmstrip extends Component <Props> {
class Filmstrip extends PureComponent <Props> {
/**
* Initializes a new {@code Filmstrip} instance.
@ -121,6 +135,8 @@ class Filmstrip extends Component <Props> {
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
this._onTabIn = this._onTabIn.bind(this);
this._gridItemKey = this._gridItemKey.bind(this);
this._listItemKey = this._listItemKey.bind(this);
}
/**
@ -154,11 +170,7 @@ class Filmstrip extends Component <Props> {
*/
render() {
const filmstripStyle = { };
const filmstripRemoteVideosContainerStyle = {};
let remoteVideoContainerClassName = 'remote-videos-container';
const { _currentLayout, _participants } = this.props;
const remoteParticipants = _participants.filter(p => !p.local);
const localParticipant = getLocalParticipant(_participants);
const { _currentLayout } = this.props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_currentLayout) {
@ -167,28 +179,11 @@ class Filmstrip extends Component <Props> {
// Also adding 7px for the scrollbar.
filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
break;
case LAYOUTS.TILE_VIEW: {
// The size of the side margins for each tile as set in CSS.
const { _columns, _rows, _filmstripWidth } = this.props;
if (_rows > _columns) {
remoteVideoContainerClassName += ' has-overflow';
}
filmstripRemoteVideosContainerStyle.width = _filmstripWidth;
break;
}
}
let remoteVideosWrapperClassName = 'filmstrip__videos';
if (this.props._hideScrollbar) {
remoteVideosWrapperClassName += ' hide-scrollbar';
}
let toolbar = null;
if (!this.props._hideToolbar && this.props._isFilmstripButtonEnabled) {
if (this.props._isFilmstripButtonEnabled) {
toolbar = this._renderToggleButton();
}
@ -206,41 +201,15 @@ class Filmstrip extends Component <Props> {
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
key = 'local' />
}
</div>
</div>
<div
className = { remoteVideosWrapperClassName }
id = 'filmstripRemoteVideos'>
{/*
* XXX This extra video container is needed for
* scrolling thumbnails in Firefox; otherwise, the flex
* thumbnails resize instead of causing overflow.
*/}
<div
className = { remoteVideoContainerClassName }
id = 'filmstripRemoteVideosContainer'
style = { filmstripRemoteVideosContainerStyle }>
{
remoteParticipants.map(
p => (
<Thumbnail
key = { `remote_${p.id}` }
participantID = { p.id } />
))
}
<div id = 'localVideoTileViewContainer'>
{
tileViewActive && <Thumbnail
key = 'local'
participantID = { localParticipant.id } />
}
</div>
</div>
</div>
{
this._renderRemoteParticipants()
}
</div>
<AudioTracksContainer />
</div>
);
}
@ -258,6 +227,135 @@ class Filmstrip extends Component <Props> {
}
}
_listItemKey: number => string;
/**
* The key to be used for every ThumbnailWrapper element in stage view.
*
* @param {number} index - The index of the ThumbnailWrapper instance.
* @returns {string} - The key.
*/
_listItemKey(index) {
const { _remoteParticipants, _remoteParticipantsLength } = this.props;
if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
return `empty-${index}`;
}
return _remoteParticipants[_remoteParticipantsLength - index - 1];
}
_gridItemKey: Object => string;
/**
* The key to be used for every ThumbnailWrapper element in tile views.
*
* @param {Object} data - An object with the indexes identifying the ThumbnailWrapper instance.
* @returns {string} - The key.
*/
_gridItemKey({ columnIndex, rowIndex }) {
const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
const index = (rowIndex * _columns) + columnIndex;
if (index > _remoteParticipantsLength) {
return `empty-${index}`;
}
if (index === _remoteParticipantsLength) {
return 'local';
}
return _remoteParticipants[index];
}
/**
* Renders the thumbnails for remote participants.
*
* @returns {ReactElement}
*/
_renderRemoteParticipants() {
const {
_columns,
_currentLayout,
_filmstripHeight,
_filmstripWidth,
_remoteParticipantsLength,
_rows,
_thumbnailHeight,
_thumbnailWidth
} = this.props;
if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
|| isNaN(_thumbnailHeight) || !_filmstripHeight || isNaN(_filmstripHeight) || !_filmstripWidth
|| isNaN(_filmstripWidth)) {
return null;
}
if (_currentLayout === LAYOUTS.TILE_VIEW) {
return (
<FixedSizeGrid
className = 'filmstrip__videos remote-videos'
columnCount = { _columns }
columnWidth = { _thumbnailWidth + TILE_HORIZONTAL_MARGIN }
height = { _filmstripHeight }
initialScrollLeft = { 0 }
initialScrollTop = { 0 }
itemKey = { this._gridItemKey }
rowCount = { _rows }
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
width = { _filmstripWidth }>
{
ThumbnailWrapper
}
</FixedSizeGrid>
);
}
const props = {
itemCount: _remoteParticipantsLength,
className: 'filmstrip__videos remote-videos',
height: _filmstripHeight,
itemKey: this._listItemKey,
itemSize: 0,
width: _filmstripWidth,
style: {
willChange: 'auto'
}
};
if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
props.itemSize = itemSize;
// $FlowFixMe
props.layout = 'horizontal';
if (isNotOverflowing) {
props.className += ' is-not-overflowing';
}
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
if (isNotOverflowing) {
props.className += ' is-not-overflowing';
}
props.itemSize = itemSize;
}
return (
<FixedSizeList { ...props }>
{
ThumbnailWrapper
}
</FixedSizeList>
);
}
/**
* Dispatches an action to change the visibility of the filmstrip.
*
@ -344,29 +442,60 @@ class Filmstrip extends Component <Props> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const toolbarButtons = getToolbarButtons(state);
const { visible } = state['features/filmstrip'];
const reduceHeight
= state['features/toolbox'].visible && toolbarButtons.length;
const { visible, remoteParticipants } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
const {
gridDimensions = {},
filmstripHeight,
filmstripWidth,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight;
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height - (reduceHeight ? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer } = state['features/filmstrip'].horizontalViewDimensions;
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
}
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_participants: state['features/base/participants'],
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_videosClassName: videosClassName,
_visible: visible,
_isToolboxVisible: isToolboxVisible(state)

View File

@ -7,7 +7,6 @@ import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import AudioTrack from '../../../base/media/components/web/AudioTrack';
import {
getLocalParticipant,
getParticipantById,
@ -28,6 +27,7 @@ import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
import { setVolume } from '../../actions.web';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_MODE_TO_STRING,
@ -65,12 +65,7 @@ export type State = {|
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean,
/**
* The current volume setting for the Thumbnail.
*/
volume: ?number
isHovered: boolean
|};
/**
@ -179,9 +174,9 @@ export type Props = {|
_participant: Object,
/**
* The number of participants in the call.
* True if there are more than 2 participants in the call.
*/
_participantCount: number,
_participantCountMoreThan2: boolean,
/**
* Indicates whether the "start silent" mode is enabled.
@ -193,6 +188,11 @@ export type Props = {|
*/
_videoTrack: ?Object,
/**
* The volume level for the thumbnail.
*/
_volume?: ?number,
/**
* The width of the thumbnail.
*/
@ -203,10 +203,20 @@ export type Props = {|
*/
dispatch: Function,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
*/
horizontalOffset: number,
/**
* The ID of the participant related to the thumbnail.
*/
participantID: ?string
participantID: ?string,
/**
* Styles that will be set to the Thumbnail's main span element.
*/
style?: ?Object
|};
/**
@ -240,7 +250,6 @@ class Thumbnail extends Component<Props, State> {
audioLevel: 0,
canPlayEventReceived: false,
isHovered: false,
volume: undefined,
displayMode: DISPLAY_VIDEO
};
@ -253,7 +262,6 @@ class Thumbnail extends Component<Props, State> {
this._onCanPlay = this._onCanPlay.bind(this);
this._onClick = this._onClick.bind(this);
this._onVolumeChange = this._onVolumeChange.bind(this);
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this._onTestingEvent = this._onTestingEvent.bind(this);
@ -457,7 +465,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {Object} - The styles for the thumbnail.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
const { _height, _isHidden, _width, style, horizontalOffset } = this.props;
let styles: {
thumbnail: Object,
avatar: Object
@ -466,39 +474,28 @@ class Thumbnail extends Component<Props, State> {
avatar: {}
};
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const avatarSize = _height / 2;
const avatarSize = _height / 2;
let { left } = style || {};
styles = {
thumbnail: {
height: `${_height}px`,
minHeight: `${_height}px`,
minWidth: `${_width}px`,
width: `${_width}px`
},
avatar: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
thumbnail: {
paddingTop: `${_heightToWidthPercent}%`
},
avatar: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
};
break;
}
if (typeof left === 'number' && horizontalOffset) {
left += horizontalOffset;
}
styles = {
thumbnail: {
...style,
left,
height: `${_height}px`,
minHeight: `${_height}px`,
minWidth: `${_width}px`,
width: `${_width}px`
},
avatar: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
if (_isHidden) {
styles.thumbnail.display = 'none';
}
@ -584,7 +581,7 @@ class Thumbnail extends Component<Props, State> {
_isDominantSpeakerDisabled,
_indicatorIconSize: iconSize,
_participant,
_participantCount
_participantCountMoreThan2
} = this.props;
const { isHovered } = this.state;
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
@ -621,7 +618,7 @@ class Thumbnail extends Component<Props, State> {
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
{ showDominantSpeaker && _participantCountMoreThan2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
@ -793,21 +790,19 @@ class Thumbnail extends Component<Props, State> {
*/
_renderRemoteParticipant() {
const {
_audioTrack,
_isTestModeEnabled,
_participant,
_startSilent,
_videoTrack
_videoTrack,
_volume = 1
} = this.props;
const { id } = _participant;
const { audioLevel, canPlayEventReceived, volume } = this.state;
const { audioLevel, canPlayEventReceived } = this.state;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
// hide volume when in silent mode
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
const jitsiAudioTrack = _audioTrack?.jitsiTrack;
const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
const videoEventListeners = {};
@ -840,14 +835,6 @@ class Thumbnail extends Component<Props, State> {
style = { videoElementStyle }
videoTrack = { _videoTrack } />
}
{
_audioTrack && <AudioTrack
audioTrack = { _audioTrack }
id = { `remoteAudio_${audioTrackId || ''}` }
muted = { _startSilent }
onInitialVolumeSet = { this._onInitialVolumeSet }
volume = { volume } />
}
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
{ this._renderTopIndicators() }
@ -872,7 +859,7 @@ class Thumbnail extends Component<Props, State> {
</span>
<span className = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { volume }
initialVolumeValue = { _volume }
onVolumeChange = { onVolumeChange }
participantID = { id } />
</span>
@ -880,20 +867,6 @@ class Thumbnail extends Component<Props, State> {
);
}
_onInitialVolumeSet: Object => void;
/**
* A handler for the initial volume value of the audio element.
*
* @param {number} volume - Properties of the audio element.
* @returns {void}
*/
_onInitialVolumeSet(volume) {
if (this.state.volume !== volume) {
this.setState({ volume });
}
}
_onVolumeChange: number => void;
/**
@ -903,7 +876,10 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_onVolumeChange(value) {
this.setState({ volume: value });
const { _participant, dispatch } = this.props;
const { id } = _participant;
dispatch(setVolume(id, value));
}
/**
@ -949,6 +925,7 @@ function _mapStateToProps(state, ownProps): Object {
const { id } = participant;
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const { participantsVolume } = state['features/filmstrip'];
const _videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
@ -967,14 +944,21 @@ function _mapStateToProps(state, ownProps): Object {
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const {
horizontalViewDimensions = {
local: {},
remote: {}
},
verticalViewDimensions = {
local: {},
remote: {}
}
} = state['features/filmstrip'];
const { local, remote } = horizontalViewDimensions;
const { local, remote }
= _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
? verticalViewDimensions : horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
size = {
@ -984,13 +968,6 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
size = {
_heightToWidthPercent: isLocal
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
};
break;
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
@ -1020,9 +997,10 @@ function _mapStateToProps(state, ownProps): Object {
_indicatorIconSize: NORMAL,
_localFlipX: Boolean(localFlipX),
_participant: participant,
_participantCount: getParticipantCount(state),
_participantCountMoreThan2: getParticipantCount(state) > 2,
_startSilent: Boolean(startSilent),
_videoTrack,
_volume: isLocal ? undefined : participantsVolume[id],
...size
};
}

View File

@ -0,0 +1,155 @@
/* @flow */
import React, { Component } from 'react';
import { shouldComponentUpdate } from 'react-window';
import { connect } from '../../../base/redux';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import Thumbnail from './Thumbnail';
/**
* The type of the React {@code Component} props of {@link ThumbnailWrapper}.
*/
type Props = {
/**
* The ID of the participant associated with the Thumbnail.
*/
_participantID: ?string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
*/
_horizontalOffset: number,
/**
* The index of the column in tile view.
*/
columnIndex?: number,
/**
* The index of the ThumbnailWrapper in stage view.
*/
index?: number,
/**
* The index of the row in tile view.
*/
rowIndex?: number,
/**
* The styles comming from react-window.
*/
style: Object
};
/**
* A wrapper Component for the Thumbnail that translates the react-window specific props
* to the Thumbnail Component's props.
*/
class ThumbnailWrapper extends Component<Props> {
/**
* Creates new ThumbnailWrapper instance.
*
* @param {Props} props - The props of the component.
*/
constructor(props: Props) {
super(props);
this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
}
shouldComponentUpdate: Props => boolean;
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _participantID, style, _horizontalOffset = 0 } = this.props;
if (typeof _participantID !== 'string') {
return null;
}
if (_participantID === 'local') {
return (
<Thumbnail
horizontalOffset = { _horizontalOffset }
key = 'local'
style = { style } />);
}
return (
<Thumbnail
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
style = { style } />);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code ThumbnailWrapper}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The props passed to the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants } = state['features/filmstrip'];
const remoteParticipantsLength = remoteParticipants.length;
if (_currentLayout === LAYOUTS.TILE_VIEW) {
const { columnIndex, rowIndex } = ownProps;
const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns;
if (participantsInTheLastRow > 0) {
horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2);
}
}
if (index > remoteParticipantsLength) {
return {};
}
if (index === remoteParticipantsLength) {
return {
_participantID: 'local',
_horizontalOffset: horizontalOffset
};
}
return {
_participantID: remoteParticipants[index],
_horizontalOffset: horizontalOffset
};
}
const { index } = ownProps;
if (typeof index !== 'number' || remoteParticipantsLength <= index) {
return {};
}
return {
_participantID: remoteParticipants[index]
};
}
export default connect(_mapStateToProps)(ThumbnailWrapper);

View File

@ -143,3 +143,68 @@ export const DISPLAY_MODE_TO_STRING = [
'video-with-name',
'avatar-with-name'
];
/**
* The vertical margin of a tile.
*
* @type {number}
*/
export const TILE_VERTICAL_MARGIN = 4;
/**
* The horizontal margin of a tile.
*
* @type {number}
*/
export const TILE_HORIZONTAL_MARGIN = 4;
/**
* The height of the whole toolbar.
*/
export const TOOLBAR_HEIGHT = 72;
/**
* The size of the horizontal border of a thumbnail.
*
* @type {number}
*/
export const STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER = 4;
/**
* The size of the vertical border of a thumbnail.
*
* @type {number}
*/
export const STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER = 4;
/**
* The size of the scroll.
*
* @type {number}
*/
export const SCROLL_SIZE = 7;
/**
* The total vertical space between the thumbnails container and the edges of the window.
*
* NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
*
* @type {number}
*/
export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 60;
/**
* The min horizontal space between the thumbnails container and the edges of the window.
*
* @type {number}
*/
export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
/**
* The total horizontal space between the thumbnails container and the edges of the window.
*
* NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
*
* @type {number}
*/
export const HORIZONTAL_FILMSTRIP_MARGIN = 39;

View File

@ -23,16 +23,17 @@ import {
DISPLAY_BLACKNESS_WITH_NAME,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
TILE_ASPECT_RATIO
STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
TILE_ASPECT_RATIO,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
} from './constants';
declare var interfaceConfig: Object;
// Minimum space to keep between the sides of the tiles and the sides
// of the window.
const TILE_VIEW_SIDE_MARGINS = 20;
/**
* Returns true if the filmstrip on mobile is visible, false otherwise.
*
@ -139,15 +140,42 @@ export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0
};
}
/**
* Calculates the size for thumbnails when in vertical view layout.
*
* @param {number} clientWidth - The height of the app window.
* @returns {{local: {height, width}, remote: {height, width}}}
*/
export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
const horizontalMargin
= VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN + SCROLL_SIZE
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
const availableWidth = Math.min(
Math.max(clientWidth - horizontalMargin, 0),
interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120);
return {
local: {
height: Math.floor(availableWidth / interfaceConfig.LOCAL_THUMBNAIL_RATIO),
width: availableWidth
},
remote: {
height: Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
width: availableWidth
}
};
}
/**
* Calculates the size for thumbnails when in tile view layout.
*
* @param {Object} dimensions - The desired dimensions of the tile view grid.
* @returns {{height, width}}
* @returns {{hasScroll, height, width}}
*/
export function calculateThumbnailSizeForTileView({
columns,
visibleRows,
minVisibleRows,
rows,
clientWidth,
clientHeight,
disableResponsiveTiles
@ -158,12 +186,29 @@ export function calculateThumbnailSizeForTileView({
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
}
const viewWidth = clientWidth - TILE_VIEW_SIDE_MARGINS;
const viewHeight = clientHeight - TILE_VIEW_SIDE_MARGINS;
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
const initialWidth = viewWidth / columns;
const initialHeight = viewHeight / minVisibleRows;
const aspectRatioHeight = initialWidth / aspectRatio;
const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows));
const width = Math.floor(aspectRatio * height);
const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
let width = Math.floor(aspectRatio * height);
if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
// Recalculating width/height to fit the available space when a scroll is displayed.
// NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
// height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
// to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
// bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
// window.
height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
width = Math.floor(aspectRatio * height);
}
return {
height,

View File

@ -9,7 +9,7 @@ import {
LAYOUTS
} from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
import './subscriber.web';
@ -27,22 +27,16 @@ MiddlewareRegistry.register(store => next => action => {
switch (layout) {
case LAYOUTS.TILE_VIEW: {
const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
store.dispatch(setHorizontalViewDimensions());
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
break;
}
break;

View File

@ -1,12 +1,15 @@
// @flow
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS
SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VOLUME
} from './actionTypes';
const DEFAULT_STATE = {
@ -26,6 +29,21 @@ const DEFAULT_STATE = {
*/
horizontalViewDimensions: {},
/**
* The custom audio volume levels per perticipant.
*
* @type {Object}
*/
participantsVolume: {},
/**
* The ordered IDs of the remote participants displayed in the filmstrip.
*
* NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
* reordering the remote participants.
*/
remoteParticipants: [],
/**
* The tile view dimensions.
*
@ -34,6 +52,14 @@ const DEFAULT_STATE = {
*/
tileViewDimensions: {},
/**
* The vertical view dimensions.
*
* @public
* @type {Object}
*/
verticalViewDimensions: {},
/**
* The indicator which determines whether the {@link Filmstrip} is visible.
*
@ -69,6 +95,44 @@ ReducerRegistry.register(
...state,
tileViewDimensions: action.dimensions
};
case SET_VERTICAL_VIEW_DIMENSIONS:
return {
...state,
verticalViewDimensions: action.dimensions
};
case SET_VOLUME:
return {
...state,
participantsVolume: {
...state.participantsVolume,
// NOTE: This would fit better in the features/base/participants. But currently we store
// the participants as an array which will make it expensive to search for the volume for
// every participant separately.
[action.participantId]: action.volume
}
};
case PARTICIPANT_JOINED: {
const { id, local } = action.participant;
if (!local) {
state.remoteParticipants = [ ...state.remoteParticipants, id ];
}
return state;
}
case PARTICIPANT_LEFT: {
const { id, local } = action.participant;
if (local) {
return state;
}
state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id);
delete state.participantsVolume[id];
return state;
}
}
return state;

View File

@ -7,7 +7,7 @@ import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_DRAWER_THRESHOLD,
@ -28,18 +28,7 @@ StateListenerRegistry.register(
const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
if (!equals(gridDimensions, oldGridDimensions)) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
}
}
});
@ -53,23 +42,14 @@ StateListenerRegistry.register(
const state = store.getState();
switch (layout) {
case LAYOUTS.TILE_VIEW: {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
getTileViewGridDimensions(state),
{
clientHeight,
clientWidth
},
store
)
);
case LAYOUTS.TILE_VIEW:
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
store.dispatch(setHorizontalViewDimensions());
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
break;
}
});
@ -168,17 +148,7 @@ StateListenerRegistry.register(
if (shouldDisplayTileView(state)) {
const gridDimensions = getTileViewGridDimensions(state);
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
store.dispatch(
setTileViewDimensions(
gridDimensions,
{
clientHeight,
clientWidth
},
store
)
);
store.dispatch(setTileViewDimensions(gridDimensions));
}
});

View File

@ -106,11 +106,12 @@ export function getTileViewGridDimensions(state: Object) {
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
const visibleRows = Math.min(maxColumns, rows);
const minVisibleRows = Math.min(maxColumns, rows);
return {
columns,
visibleRows
minVisibleRows,
rows
};
}