Skip to content

Commit

Permalink
feat: auto tile view
Browse files Browse the repository at this point in the history
  • Loading branch information
zbettenbuk committed Jul 24, 2020
1 parent 00b41db commit 240b033
Show file tree
Hide file tree
Showing 22 changed files with 188 additions and 83 deletions.
3 changes: 2 additions & 1 deletion modules/UI/shared_video/SharedVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getToolboxHeight,
showToolbox
} from '../../../react/features/toolbox';
import { YOUTUBE_PARTICIPANT_NAME } from '../../../react/features/youtube-player';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import Filmstrip from '../videolayout/Filmstrip';
Expand Down Expand Up @@ -305,7 +306,7 @@ export default class SharedVideoManager {
conference: APP.conference._room,
id: self.url,
isFakeParticipant: true,
name: 'YouTube'
name: YOUTUBE_PARTICIPANT_NAME
}));

APP.store.dispatch(pinParticipant(self.url));
Expand Down
4 changes: 2 additions & 2 deletions modules/UI/videolayout/SmallVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export default class SmallVideo {
*/
selectDisplayMode(input) {
// Display name is always and only displayed when user is on the stage
if (input.isCurrentlyOnLargeVideo && !input.tileViewEnabled) {
if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
// check hovering and change state to video with name
Expand All @@ -472,7 +472,7 @@ export default class SmallVideo {
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
isHovered: this._isHovered(),
isAudioOnly: APP.conference.isAudioOnly(),
tileViewEnabled: shouldDisplayTileView(APP.store.getState()),
tileViewActive: shouldDisplayTileView(APP.store.getState()),
isVideoPlayable: this.isVideoPlayable(),
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
Expand Down
4 changes: 3 additions & 1 deletion react/features/base/lastn/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from '../../large-video/actionTypes';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { SCREEN_SHARE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-layout/actionTypes';
import { shouldDisplayTileView } from '../../video-layout/functions';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import { getParticipantById } from '../participants/functions';
Expand Down Expand Up @@ -59,7 +60,8 @@ function _updateLastN({ getState }) {
if (typeof appState !== 'undefined' && appState !== 'active') {
lastN = 0;
} else if (audioOnly) {
const { screenShares, tileViewEnabled } = state['features/video-layout'];
const { screenShares } = state['features/video-layout'];
const tileViewEnabled = shouldDisplayTileView(state);
const largeVideoParticipantId = state['features/large-video'].participantId;
const largeVideoParticipant
= largeVideoParticipantId ? getParticipantById(state, largeVideoParticipantId) : undefined;
Expand Down
3 changes: 2 additions & 1 deletion react/features/conference/components/web/InviteMore.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isButtonEnabled,
isToolboxVisible
} from '../../../toolbox';
import { shouldDisplayTileView } from '../../../video-layout/functions';

declare var interfaceConfig: Object;

Expand Down Expand Up @@ -83,7 +84,7 @@ function mapStateToProps(state) {
const hide = interfaceConfig.HIDE_INVITE_MORE_HEADER;

return {
_tileViewEnabled: state['features/video-layout'].tileViewEnabled,
_tileViewEnabled: shouldDisplayTileView(state),
_visible: isToolboxVisible(state) && isButtonEnabled('invite') && isAlone && !hide
};
}
Expand Down
9 changes: 3 additions & 6 deletions react/features/follow-me/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,14 @@ function _onFollowMeCommand(attributes = {}, id, store) {
}
}

const pinnedParticipant
= getPinnedParticipant(state, attributes.nextOnStage);
const pinnedParticipant = getPinnedParticipant(state);
const idOfParticipantToPin = attributes.nextOnStage;

if (typeof idOfParticipantToPin !== 'undefined'
&& (!pinnedParticipant
|| idOfParticipantToPin !== pinnedParticipant.id)
&& (!pinnedParticipant || idOfParticipantToPin !== pinnedParticipant.id)
&& oldState.nextOnStage !== attributes.nextOnStage) {
_pinVideoThumbnailById(store, idOfParticipantToPin);
} else if (typeof idOfParticipantToPin === 'undefined'
&& pinnedParticipant) {
} else if (typeof idOfParticipantToPin === 'undefined' && pinnedParticipant) {
store.dispatch(pinParticipant(null));
}
}
Expand Down
3 changes: 2 additions & 1 deletion react/features/follow-me/subscriber.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isLocalParticipantModerator
} from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { shouldDisplayTileView } from '../video-layout/functions';

import { FOLLOW_ME_COMMAND } from './constants';

Expand Down Expand Up @@ -72,7 +73,7 @@ function _getFollowMeState(state) {
filmstripVisible: state['features/filmstrip'].visible,
nextOnStage: pinnedParticipant && pinnedParticipant.id,
sharedDocumentVisible: state['features/etherpad'].editing,
tileViewEnabled: state['features/video-layout'].tileViewEnabled
tileViewEnabled: shouldDisplayTileView(state)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IconPin } from '../../../base/icons';
import { pinParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox';
import { shouldDisplayTileView } from '../../../video-layout/functions';

export type Props = AbstractButtonProps & {

Expand Down Expand Up @@ -59,7 +60,7 @@ class PinButton extends AbstractButton<Props, *> {
*/
function _mapStateToProps(state) {
return {
visible: state['features/video-layout'].tileViewEnabled
visible: shouldDisplayTileView(state)
};
}

Expand Down
3 changes: 2 additions & 1 deletion react/features/toolbox/components/web/Toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
} from '../../../subtitles';
import {
TileViewButton,
shouldDisplayTileView,
toggleTileView
} from '../../../video-layout';
import {
Expand Down Expand Up @@ -1416,7 +1417,7 @@ function _mapStateToProps(state) {
_feedbackConfigured: Boolean(callStatsID),
_isGuest: state['features/base/jwt'].isGuest,
_fullScreen: fullScreen,
_tileViewEnabled: state['features/video-layout'].tileViewEnabled,
_tileViewEnabled: shouldDisplayTileView(state),
_localParticipantID: localParticipant.id,
_localRecState: localRecordingStates,
_locked: locked,
Expand Down
9 changes: 5 additions & 4 deletions react/features/video-layout/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SCREEN_SHARE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from './actionTypes';
import { shouldDisplayTileView } from './functions';

/**
* Creates a (redux) action which signals that the list of known participants
Expand All @@ -32,10 +33,10 @@ export function setParticipantsWithScreenShare(participantIds: Array<string>) {
* @param {boolean} enabled - Whether or not tile view should be shown.
* @returns {{
* type: SET_TILE_VIEW,
* enabled: boolean
* enabled: ?boolean
* }}
*/
export function setTileView(enabled: boolean) {
export function setTileView(enabled: ?boolean) {
return {
type: SET_TILE_VIEW,
enabled
Expand All @@ -50,8 +51,8 @@ export function setTileView(enabled: boolean) {
*/
export function toggleTileView() {
return (dispatch: Dispatch<any>, getState: Function) => {
const { tileViewEnabled } = getState()['features/video-layout'];
const tileViewActive = shouldDisplayTileView(getState());

dispatch(setTileView(!tileViewEnabled));
dispatch(setTileView(!tileViewActive));
};
}
7 changes: 5 additions & 2 deletions react/features/video-layout/components/TileViewButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
import { TILE_VIEW_ENABLED, getFeatureFlag } from '../../base/flags';
import { translate } from '../../base/i18n';
import { IconTileView } from '../../base/icons';
import { getParticipantCount } from '../../base/participants';
import { connect } from '../../base/redux';
import {
AbstractButton,
type AbstractButtonProps
} from '../../base/toolbox';
import { setTileView } from '../actions';
import { shouldDisplayTileView } from '../functions';
import logger from '../logger';

/**
Expand Down Expand Up @@ -88,10 +90,11 @@ class TileViewButton<P: Props> extends AbstractButton<P, *> {
*/
function _mapStateToProps(state, ownProps) {
const enabled = getFeatureFlag(state, TILE_VIEW_ENABLED, true);
const { visible = enabled } = ownProps;
const lonelyMeeting = getParticipantCount(state) < 2;
const { visible = enabled && !lonelyMeeting } = ownProps;

return {
_tileViewEnabled: state['features/video-layout'].tileViewEnabled,
_tileViewEnabled: shouldDisplayTileView(state),
visible
};
}
Expand Down
54 changes: 41 additions & 13 deletions react/features/video-layout/functions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { getPinnedParticipant } from '../base/participants';
import { getPinnedParticipant, getParticipantCount } from '../base/participants';
import { isYoutubeVideoPlaying } from '../youtube-player';

import { LAYOUTS } from './constants';

Expand Down Expand Up @@ -72,17 +73,44 @@ export function getTileViewGridDimensions(state: Object, maxColumns: number = ge
* @returns {boolean} True if tile view should be displayed.
*/
export function shouldDisplayTileView(state: Object = {}) {
return Boolean(
state['features/video-layout']
&& state['features/video-layout'].tileViewEnabled
&& (!state['features/etherpad']
|| !state['features/etherpad'].editing)

// Truthy check is needed for interfaceConfig to prevent errors on
// mobile which does not have interfaceConfig. On web, tile view
// should never be enabled for filmstrip only mode.
&& (typeof interfaceConfig === 'undefined'
|| !interfaceConfig.filmStripOnly)
&& !getPinnedParticipant(state)
const participantCount = getParticipantCount(state);

// In case of a lonely meeting, we don't allow tile view.
// But it's a special case too, as we don't even render the button,
// see TileViewButton component.
if (participantCount < 2) {
return false;
}

const { tileViewEnabled } = state['features/video-layout'];

if (tileViewEnabled !== undefined) {
// If the user explicitly requested a view mode, we
// do that.
return tileViewEnabled;
}

// None tile view mode is easier to calculate (no need for many negations), so we do
// that and negate it only once.
const shouldDisplayNormalMode = Boolean(

// Reasons for normal mode:

// Editing etherpad
state['features/etherpad']?.editing

// We're in filmstrip-only mode
|| (typeof interfaceConfig === 'object' && interfaceConfig?.filmStripOnly)

// We pinned a participant
|| getPinnedParticipant(state)

// It's a 1-on-1 meeting
|| participantCount < 3

// There is a shared YouTube video in the meeting
|| isYoutubeVideoPlaying(state)
);

return !shouldDisplayNormalMode;
}
98 changes: 70 additions & 28 deletions react/features/video-layout/middleware.any.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,100 @@
import {
PIN_PARTICIPANT,
getPinnedParticipant,
pinParticipant
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { SET_DOCUMENT_EDITING_STATUS, toggleDocument } from '../etherpad';
// @flow

import { getCurrentConference } from '../base/conference';
import { PIN_PARTICIPANT, pinParticipant, getPinnedParticipant } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { SET_DOCUMENT_EDITING_STATUS } from '../etherpad';

import { SET_TILE_VIEW } from './actionTypes';
import { setTileView } from './actions';

import './subscriber';

let previousTileViewEnabled;

/**
* Middleware which intercepts actions and updates tile view related state.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);

switch (action.type) {

// Actions that temporarily clear the user preferred state of tile view,
// then re-set it when needed.
case PIN_PARTICIPANT: {
const isPinning = Boolean(action.participant.id);
const { tileViewEnabled } = store.getState()['features/video-layout'];
const pinnedParticipant = getPinnedParticipant(store.getState());

if (isPinning && tileViewEnabled) {
store.dispatch(setTileView(false));
if (pinnedParticipant) {
_storeTileViewStateAndClear(store);
} else {
_restoreTileViewState(store);
}

break;
}

case SET_DOCUMENT_EDITING_STATUS:
if (action.editing) {
store.dispatch(setTileView(false));
_storeTileViewStateAndClear(store);
} else {
_restoreTileViewState(store);
}

break;

case SET_TILE_VIEW: {
const state = store.getState();
// Things to update when tile view state changes
case SET_TILE_VIEW:
if (action.enabled && getPinnedParticipant(store)) {
store.dispatch(pinParticipant(null));
}
}


if (action.enabled) {
if (getPinnedParticipant(state)) {
store.dispatch(pinParticipant(null));
}
return result;
});

if (state['features/etherpad'].editing) {
store.dispatch(toggleDocument());
}
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference) => {
if (conference !== previousConference) {
// conference changed, left or failed...
// Clear tile view state.
dispatch(setTileView());
}
});

break;
}
/**
* Respores tile view state, if it wasn't updated since then.
*
* @param {Object} store - The Redux Store.
* @returns {void}
*/
function _restoreTileViewState({ dispatch, getState }) {
const { tileViewEnabled } = getState()['features/video-layout'];

if (tileViewEnabled === undefined && previousTileViewEnabled !== undefined) {
dispatch(setTileView(previousTileViewEnabled));
}

return next(action);
});
previousTileViewEnabled = undefined;
}

/**
* Stores the current tile view state and clears it.
*
* @param {Object} store - The Redux Store.
* @returns {void}
*/
function _storeTileViewStateAndClear({ dispatch, getState }) {
const { tileViewEnabled } = getState()['features/video-layout'];

if (tileViewEnabled !== undefined) {
previousTileViewEnabled = tileViewEnabled;
dispatch(setTileView(undefined));
}
}
Loading

0 comments on commit 240b033

Please sign in to comment.