diff --git a/react/features/filmstrip/actionTypes.js b/react/features/filmstrip/actionTypes.js index 9551ff64e..c74d00151 100644 --- a/react/features/filmstrip/actionTypes.js +++ b/react/features/filmstrip/actionTypes.js @@ -70,3 +70,15 @@ export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS'; * } */ export const SET_VOLUME = 'SET_VOLUME'; + +/** + * The type of the action which sets the list of visible remote participants in the filmstrip by storing the start and + * end index in the remote participants array. + * + * { + * type: SET_VISIBLE_REMOTE_PARTICIPANTS, + * startIndex: number, + * endIndex: number + * } + */ +export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS'; diff --git a/react/features/filmstrip/actions.web.js b/react/features/filmstrip/actions.web.js index 9bfe043b0..a3bfb8211 100644 --- a/react/features/filmstrip/actions.web.js +++ b/react/features/filmstrip/actions.web.js @@ -7,6 +7,7 @@ import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS, + SET_VISIBLE_REMOTE_PARTICIPANTS, SET_VOLUME } from './actionTypes'; import { @@ -153,4 +154,24 @@ export function setVolume(participantId: string, volume: number) { }; } +/** + * Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote + * participants array. + * + * @param {number} startIndex - The start index from the remote participants array. + * @param {number} endIndex - The end index from the remote participants array. + * @returns {{ + * type: SET_VISIBLE_REMOTE_PARTICIPANTS, + * startIndex: number, + * endIndex: number + * }} + */ +export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) { + return { + type: SET_VISIBLE_REMOTE_PARTICIPANTS, + startIndex, + endIndex + }; +} + export * from './actions.native'; diff --git a/react/features/filmstrip/components/web/Filmstrip.js b/react/features/filmstrip/components/web/Filmstrip.js index f6e3afce8..53f1ec905 100644 --- a/react/features/filmstrip/components/web/Filmstrip.js +++ b/react/features/filmstrip/components/web/Filmstrip.js @@ -16,7 +16,7 @@ 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 { setFilmstripVisible, setVisibleRemoteParticipants } from '../../actions'; import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants'; import { shouldRemoteVideosBeVisible } from '../../functions'; @@ -67,7 +67,6 @@ type Props = { */ _remoteParticipants: Array, - /** * The length of the remote participants array. */ @@ -137,6 +136,8 @@ class Filmstrip extends PureComponent { this._onTabIn = this._onTabIn.bind(this); this._gridItemKey = this._gridItemKey.bind(this); this._listItemKey = this._listItemKey.bind(this); + this._onGridItemsRendered = this._onGridItemsRendered.bind(this); + this._onListItemsRendered = this._onListItemsRendered.bind(this); } /** @@ -268,6 +269,41 @@ class Filmstrip extends PureComponent { return _remoteParticipants[index]; } + _onListItemsRendered: Object => void; + + /** + * Handles items rendered changes in stage view. + * + * @param {Object} data - Information about the rendered items. + * @returns {void} + */ + _onListItemsRendered({ overscanStartIndex, overscanStopIndex }) { + const { dispatch } = this.props; + + dispatch(setVisibleRemoteParticipants(overscanStartIndex, overscanStopIndex)); + } + + _onGridItemsRendered: Object => void; + + /** + * Handles items rendered changes in tile view. + * + * @param {Object} data - Information about the rendered items. + * @returns {void} + */ + _onGridItemsRendered({ + overscanColumnStartIndex, + overscanColumnStopIndex, + overscanRowStartIndex, + overscanRowStopIndex + }) { + const { _columns, dispatch } = this.props; + const startIndex = (overscanRowStartIndex * _columns) + overscanColumnStartIndex; + const endIndex = (overscanRowStopIndex * _columns) + overscanColumnStopIndex; + + dispatch(setVisibleRemoteParticipants(startIndex, endIndex)); + } + /** * Renders the thumbnails for remote participants. * @@ -301,6 +337,7 @@ class Filmstrip extends PureComponent { initialScrollLeft = { 0 } initialScrollTop = { 0 } itemKey = { this._gridItemKey } + onItemsRendered = { this._onGridItemsRendered } rowCount = { _rows } rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN } width = { _filmstripWidth }> @@ -318,6 +355,7 @@ class Filmstrip extends PureComponent { height: _filmstripHeight, itemKey: this._listItemKey, itemSize: 0, + onItemsRendered: this._onListItemsRendered, width: _filmstripWidth, style: { willChange: 'auto' diff --git a/react/features/filmstrip/components/web/ThumbnailWrapper.js b/react/features/filmstrip/components/web/ThumbnailWrapper.js index 33e7e67a4..b9c197cda 100644 --- a/react/features/filmstrip/components/web/ThumbnailWrapper.js +++ b/react/features/filmstrip/components/web/ThumbnailWrapper.js @@ -12,16 +12,16 @@ import Thumbnail from './Thumbnail'; */ type Props = { + /** + * The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view. + */ + _horizontalOffset: number, + /** * 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. */ @@ -114,12 +114,11 @@ function _mapStateToProps(state, ownProps) { if (rowIndex === rows - 1) { // center the last row const { width: thumbnailWidth } = thumbnailSize; - const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns; + const partialLastRowParticipantsNumber = (remoteParticipantsLength + 1) % columns; - if (participantsInTheLastRow > 0) { - horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2); + if (partialLastRowParticipantsNumber > 0) { + horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2); } - } if (index > remoteParticipantsLength) { @@ -133,12 +132,10 @@ function _mapStateToProps(state, ownProps) { }; } - return { _participantID: remoteParticipants[index], _horizontalOffset: horizontalOffset }; - } const { index } = ownProps; diff --git a/react/features/filmstrip/reducer.js b/react/features/filmstrip/reducer.js index 40881cdec..69b81d54b 100644 --- a/react/features/filmstrip/reducer.js +++ b/react/features/filmstrip/reducer.js @@ -9,6 +9,7 @@ import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS, + SET_VISIBLE_REMOTE_PARTICIPANTS, SET_VOLUME } from './actionTypes'; @@ -66,7 +67,32 @@ const DEFAULT_STATE = { * @public * @type {boolean} */ - visible: true + visible: true, + + /** + * The end index in the remote participants array that is visible in the filmstrip. + * + * @public + * @type {number} + */ + visibleParticipantsEndIndex: 0, + + /** + * The visible participants in the filmstrip. + * + * @public + * @type {Array} + */ + visibleParticipants: [], + + + /** + * The start index in the remote participants array that is visible in the filmstrip. + * + * @public + * @type {number} + */ + visibleParticipantsStartIndex: 0 }; ReducerRegistry.register( @@ -112,11 +138,24 @@ ReducerRegistry.register( [action.participantId]: action.volume } }; + case SET_VISIBLE_REMOTE_PARTICIPANTS: + return { + ...state, + visibleParticipantsStartIndex: action.startIndex, + visibleParticipantsEndIndex: action.endIndex, + visibleParticipants: state.remoteParticipants.slice(action.startIndex, action.endIndex + 1) + }; case PARTICIPANT_JOINED: { const { id, local } = action.participant; if (!local) { state.remoteParticipants = [ ...state.remoteParticipants, id ]; + + const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state; + + if (state.remoteParticipants.length - 1 <= endIndex) { + state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1); + } } return state; @@ -128,7 +167,24 @@ ReducerRegistry.register( return state; } - state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id); + let removedParticipantIndex = 0; + + state.remoteParticipants = state.remoteParticipants.filter((participantId, index) => { + if (participantId === id) { + removedParticipantIndex = index; + + return false; + } + + return true; + }); + + const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state; + + if (removedParticipantIndex >= startIndex && removedParticipantIndex <= endIndex) { + state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1); + } + delete state.participantsVolume[id]; return state; diff --git a/react/features/large-video/actions.any.js b/react/features/large-video/actions.any.js index e3f0d2637..315500c90 100644 --- a/react/features/large-video/actions.any.js +++ b/react/features/large-video/actions.any.js @@ -3,30 +3,12 @@ import type { Dispatch } from 'redux'; import { MEDIA_TYPE } from '../base/media'; -import { getParticipants } from '../base/participants'; -import { selectEndpoints, shouldDisplayTileView } from '../video-layout'; import { SELECT_LARGE_VIDEO_PARTICIPANT, UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION } from './actionTypes'; -/** - * Signals conference to select a participant. - * - * @returns {Function} - */ -export function selectParticipant() { - return (dispatch: Dispatch, getState: Function) => { - const state = getState(); - const ids = shouldDisplayTileView(state) - ? getParticipants(state).map(participant => participant.id) - : [ state['features/large-video'].participantId ]; - - dispatch(selectEndpoints(ids)); - }; -} - /** * Action to select the participant to be displayed in LargeVideo based on the * participant id provided. If a participant id is not provided, the LargeVideo @@ -60,8 +42,6 @@ export function selectParticipantInLargeVideo(participant: ?string) { type: SELECT_LARGE_VIDEO_PARTICIPANT, participantId }); - - dispatch(selectParticipant()); } }; } diff --git a/react/features/large-video/middleware.js b/react/features/large-video/middleware.js index f5a7a0d7d..97141ab10 100644 --- a/react/features/large-video/middleware.js +++ b/react/features/large-video/middleware.js @@ -1,6 +1,5 @@ // @flow -import { CONFERENCE_JOINED } from '../base/conference'; import { DOMINANT_SPEAKER_CHANGED, PARTICIPANT_JOINED, @@ -11,13 +10,11 @@ import { import { MiddlewareRegistry } from '../base/redux'; import { isTestModeEnabled } from '../base/testing'; import { - getTrackByJitsiTrack, TRACK_ADDED, - TRACK_REMOVED, - TRACK_UPDATED + TRACK_REMOVED } from '../base/tracks'; -import { selectParticipant, selectParticipantInLargeVideo } from './actions.any'; +import { selectParticipantInLargeVideo } from './actions'; import logger from './logger'; import './subscriber'; @@ -54,30 +51,6 @@ MiddlewareRegistry.register(store => next => action => { case TRACK_REMOVED: store.dispatch(selectParticipantInLargeVideo()); break; - - case CONFERENCE_JOINED: - // Ensure a participant is selected on conference join. This addresses - // the case where video tracks were received before CONFERENCE_JOINED - // fired; without the conference selection may not happen. - store.dispatch(selectParticipant()); - break; - - case TRACK_UPDATED: - // In order to minimize re-calculations, we need to select participant - // only if the videoType of the current participant rendered in - // LargeVideo has changed. - if ('videoType' in action.track) { - const state = store.getState(); - const track - = getTrackByJitsiTrack( - state['features/base/tracks'], - action.track.jitsiTrack); - const participantId = state['features/large-video'].participantId; - - (track.participantId === participantId) - && store.dispatch(selectParticipant()); - } - break; } return result; diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 506067bdc..a952374f3 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -1363,6 +1363,7 @@ class Toolbox extends Component { { showOverflowMenuButton &&
    { } diff --git a/react/features/video-layout/actionTypes.js b/react/features/video-layout/actionTypes.js index 400297efe..0b19fa84b 100644 --- a/react/features/video-layout/actionTypes.js +++ b/react/features/video-layout/actionTypes.js @@ -10,12 +10,6 @@ export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED = 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED'; -/** - * The type of the action which sets the list of the endpoints to be selected for video forwarding - * from the bridge. - */ -export const SELECT_ENDPOINTS = 'SELECT_ENDPOINTS'; - /** * The type of the action which enables or disables the feature for showing * video thumbnails in a two-axis tile view. diff --git a/react/features/video-layout/actions.js b/react/features/video-layout/actions.js index d54e69ae4..0494589f1 100644 --- a/react/features/video-layout/actions.js +++ b/react/features/video-layout/actions.js @@ -4,28 +4,10 @@ import type { Dispatch } from 'redux'; import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, - SELECT_ENDPOINTS, SET_TILE_VIEW } from './actionTypes'; import { shouldDisplayTileView } from './functions'; -/** - * Creates a (redux) action which signals that a new set of remote endpoints need to be selected. - * - * @param {Array} participantIds - The remote participants that are currently selected - * for video forwarding from the bridge. - * @returns {{ - * type: SELECT_ENDPOINTS, - * particpantsIds: Array - * }} - */ -export function selectEndpoints(participantIds: Array) { - return { - type: SELECT_ENDPOINTS, - participantIds - }; -} - /** * Creates a (redux) action which signals that the list of known remote participants * with screen shares has changed. diff --git a/react/features/video-layout/reducer.js b/react/features/video-layout/reducer.js index f5bfe7c8c..db4e74427 100644 --- a/react/features/video-layout/reducer.js +++ b/react/features/video-layout/reducer.js @@ -4,7 +4,6 @@ import { ReducerRegistry } from '../base/redux'; import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, - SELECT_ENDPOINTS, SET_TILE_VIEW } from './actionTypes'; @@ -35,13 +34,6 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { }; } - case SELECT_ENDPOINTS: { - return { - ...state, - selectedEndpoints: action.participantIds - }; - } - case SET_TILE_VIEW: return { ...state, diff --git a/react/features/video-layout/subscriber.js b/react/features/video-layout/subscriber.js index 8534889df..4296e00aa 100644 --- a/react/features/video-layout/subscriber.js +++ b/react/features/video-layout/subscriber.js @@ -4,24 +4,10 @@ import debounce from 'lodash/debounce'; import { StateListenerRegistry, equals } from '../base/redux'; import { isFollowMeActive } from '../follow-me'; -import { selectParticipant } from '../large-video/actions.any'; import { setRemoteParticipantsWithScreenShare } from './actions'; import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions'; -/** - * StateListenerRegistry provides a reliable way of detecting changes to - * preferred layout state and dispatching additional actions. - */ -StateListenerRegistry.register( - /* selector */ state => state['features/video-layout'].tileViewEnabled, - /* listener */ (tileViewEnabled, store) => { - const { dispatch } = store; - - dispatch(selectParticipant()); - } -); - /** * For auto-pin mode, listen for changes to the known media tracks and look * for updates to screen shares. The listener is debounced to avoid state diff --git a/react/features/video-quality/constants.js b/react/features/video-quality/constants.js index 644e5cab2..dec544f75 100644 --- a/react/features/video-quality/constants.js +++ b/react/features/video-quality/constants.js @@ -14,7 +14,8 @@ export const VIDEO_QUALITY_LEVELS = { ULTRA: 2160, HIGH: 720, STANDARD: 360, - LOW: 180 + LOW: 180, + NONE: 0 }; /** diff --git a/react/features/video-quality/subscriber.js b/react/features/video-quality/subscriber.js index 854147dc8..8722e61d0 100644 --- a/react/features/video-quality/subscriber.js +++ b/react/features/video-quality/subscriber.js @@ -17,16 +17,47 @@ import { getMinHeightForQualityLvlMap } from './selector'; declare var APP: Object; /** - * StateListenerRegistry provides a reliable way of detecting changes to selected - * endpoints state and dispatching additional actions. The listener is debounced + * Handles changes in the visible participants in the filmstrip. The listener is debounced * so that the client doesn't end up sending too many bridge messages when the user is * scrolling through the thumbnails prompting updates to the selected endpoints. */ StateListenerRegistry.register( - /* selector */ state => state['features/video-layout'].selectedEndpoints, - /* listener */ debounce((selectedEndpoints, store) => { + /* selector */ state => state['features/filmstrip'].visibleParticipants, + /* listener */ debounce((visibleParticipants, store) => { _updateReceiverVideoConstraints(store); - }, 1000)); + }, 100)); + +/** + * Handles the use case when the on-stage participant has changed. + */ +StateListenerRegistry.register( + state => state['features/large-video'].participantId, + (participantId, store) => { + _updateReceiverVideoConstraints(store); + } +); + +/** + * Handles the use case when we have set some of the constraints in redux but the conference object wasn't available + * and we haven't been able to pass the constraints to lib-jitsi-meet. + */ +StateListenerRegistry.register( + state => state['features/base/conference'].conference, + (conference, store) => { + _updateReceiverVideoConstraints(store); + } +); + +/** + * Updates the receiver constraints when the layout changes. When we are in stage view we need to handle the + * on-stage participant differently. + */ +StateListenerRegistry.register( + /* selector */ state => state['features/video-layout'].tileViewEnabled, + /* listener */ (tileViewEnabled, store) => { + _updateReceiverVideoConstraints(store); + } +); /** * StateListenerRegistry provides a reliable way of detecting changes to @@ -64,6 +95,8 @@ StateListenerRegistry.register( typeof APP !== 'undefined' && APP.API.notifyVideoQualityChanged(preferredVideoQuality); } changedReceiverVideoQuality && _updateReceiverVideoConstraints(store); + }, { + deepEquals: true }); /** @@ -156,28 +189,43 @@ function _updateReceiverVideoConstraints({ getState }) { } const { lastN } = state['features/base/lastn']; const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality']; - const { selectedEndpoints } = state['features/video-layout']; + const { visibleParticipants } = state['features/filmstrip']; + const { participantId: largeVideoParticipantId } = state['features/large-video']; const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality); const receiverConstraints = { constraints: {}, - defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW }, + defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.NONE }, lastN, onStageEndpoints: [], selectedEndpoints: [] }; - if (!selectedEndpoints?.length) { - return; - } + // Tile view. + if (shouldDisplayTileView(state)) { + if (!visibleParticipants?.length) { + return; + } + + visibleParticipants.forEach(participantId => { + receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight }; + }); // Stage view. - if (selectedEndpoints?.length === 1) { - receiverConstraints.constraints[selectedEndpoints[0]] = { 'maxHeight': maxFrameHeight }; - receiverConstraints.onStageEndpoints = selectedEndpoints; - - // Tile view. } else { - receiverConstraints.defaultConstraints = { 'maxHeight': maxFrameHeight }; + if (!visibleParticipants?.length && !largeVideoParticipantId) { + return; + } + + if (visibleParticipants?.length > 0) { + visibleParticipants.forEach(participantId => { + receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW }; + }); + } + + if (largeVideoParticipantId) { + receiverConstraints.constraints[largeVideoParticipantId] = { 'maxHeight': maxFrameHeight }; + receiverConstraints.onStageEndpoints = [ largeVideoParticipantId ]; + } } logger.info(`Setting receiver video constraints to ${JSON.stringify(receiverConstraints)}`);