From 76642b7c4b18dadebce2d8cf239b598a4f5f5a77 Mon Sep 17 00:00:00 2001 From: virtuacoplenny Date: Thu, 11 Apr 2019 08:53:34 -0700 Subject: [PATCH] feat(screenshare): add auto pin of latest screen share (#4076) --- interface_config.js | 6 ++ react/features/video-layout/actionTypes.js | 12 +++ react/features/video-layout/actions.js | 23 +++++- react/features/video-layout/reducer.js | 22 +++++- react/features/video-layout/subscriber.js | 91 +++++++++++++++++++++- 5 files changed, 147 insertions(+), 7 deletions(-) diff --git a/interface_config.js b/interface_config.js index 177875321..503b54dfb 100644 --- a/interface_config.js +++ b/interface_config.js @@ -195,6 +195,12 @@ var interfaceConfig = { */ // ANDROID_APP_PACKAGE: 'org.jitsi.meet', + /** + * A UX mode where the last screen share participant is automatically + * pinned. Note: this mode is experimental and subject to breakage. + */ + // AUTO_PIN_LATEST_SCREEN_SHARE: false, + /** * Override the behavior of some notifications to remain displayed until * explicitly dismissed through a user action. The value is how long, in diff --git a/react/features/video-layout/actionTypes.js b/react/features/video-layout/actionTypes.js index 7fd139e2a..d0bcecb30 100644 --- a/react/features/video-layout/actionTypes.js +++ b/react/features/video-layout/actionTypes.js @@ -1,3 +1,15 @@ +/** + * The type of the action which sets the list of known participant IDs which + * have an active screen share. + * + * @returns {{ + * type: SCREEN_SHARE_PARTICIPANTS_UPDATED, + * participantIds: Array + * }} + */ +export const SCREEN_SHARE_PARTICIPANTS_UPDATED + = 'SCREEN_SHARE_PARTICIPANTS_UPDATED'; + /** * 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 d62bf67f2..517d5fadb 100644 --- a/react/features/video-layout/actions.js +++ b/react/features/video-layout/actions.js @@ -1,6 +1,27 @@ // @flow -import { SET_TILE_VIEW } from './actionTypes'; +import { + SCREEN_SHARE_PARTICIPANTS_UPDATED, + SET_TILE_VIEW +} from './actionTypes'; + +/** + * Creates a (redux) action which signals that the list of known participants + * with screen shares has changed. + * + * @param {string} participantIds - The participants which currently have active + * screen share streams. + * @returns {{ + * type: SCREEN_SHARE_PARTICIPANTS_UPDATED, + * participantId: string + * }} + */ +export function setParticipantsWithScreenShare(participantIds: Array) { + return { + type: SCREEN_SHARE_PARTICIPANTS_UPDATED, + participantIds + }; +} /** * Creates a (redux) action which signals to set the UI layout to be tiled view diff --git a/react/features/video-layout/reducer.js b/react/features/video-layout/reducer.js index 699320c06..3588582cc 100644 --- a/react/features/video-layout/reducer.js +++ b/react/features/video-layout/reducer.js @@ -3,14 +3,30 @@ import { ReducerRegistry } from '../base/redux'; import { PersistenceRegistry } from '../base/storage'; -import { SET_TILE_VIEW } from './actionTypes'; +import { + SCREEN_SHARE_PARTICIPANTS_UPDATED, + SET_TILE_VIEW +} from './actionTypes'; + +const DEFAULT_STATE = { + screenShares: [] +}; const STORE_NAME = 'features/video-layout'; -PersistenceRegistry.register(STORE_NAME); +PersistenceRegistry.register(STORE_NAME, { + tileViewEnabled: true +}); -ReducerRegistry.register(STORE_NAME, (state = {}, action) => { +ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { switch (action.type) { + case SCREEN_SHARE_PARTICIPANTS_UPDATED: { + return { + ...state, + screenShares: 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 01c408f6c..a7536b9c7 100644 --- a/react/features/video-layout/subscriber.js +++ b/react/features/video-layout/subscriber.js @@ -4,9 +4,16 @@ import { VIDEO_QUALITY_LEVELS, setMaxReceiverVideoQuality } from '../base/conference'; -import { StateListenerRegistry } from '../base/redux'; +import { + getPinnedParticipant, + pinParticipant +} from '../base/participants'; +import { StateListenerRegistry, equals } from '../base/redux'; import { selectParticipant } from '../large-video'; import { shouldDisplayTileView } from './functions'; +import { setParticipantsWithScreenShare } from './actions'; + +declare var interfaceConfig: Object; /** * StateListenerRegistry provides a reliable way of detecting changes to @@ -14,11 +21,89 @@ import { shouldDisplayTileView } from './functions'; */ StateListenerRegistry.register( /* selector */ state => shouldDisplayTileView(state), - /* listener */ (displayTileView, { dispatch }) => { + /* listener */ (displayTileView, store) => { + const { dispatch } = store; + dispatch(selectParticipant()); if (!displayTileView) { - dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH)); + dispatch( + setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH)); + + if (typeof interfaceConfig === 'object' + && interfaceConfig.AUTO_PIN_LATEST_SCREEN_SHARE) { + _updateAutoPinnedParticipant(store); + } } } ); + +/** + * For auto-pin mode, listen for changes to the known media tracks and look + * for updates to screen shares. + */ +StateListenerRegistry.register( + /* selector */ state => state['features/base/tracks'], + /* listener */ (tracks, store) => { + if (typeof interfaceConfig !== 'object' + && !interfaceConfig.AUTO_PIN_LATEST_SCREEN_SHARE) { + return; + } + + const oldScreenSharesOrder + = store.getState()['features/video-layout'].screenShares || []; + const knownSharingParticipantIds = tracks.reduce((acc, track) => { + if (track.mediaType === 'video' && track.videoType === 'desktop') { + acc.push(track.participantId); + } + + return acc; + }, []); + + // Filter out any participants which are no longer screen sharing + // by looping through the known sharing participants and removing any + // participant IDs which are no longer sharing. + const newScreenSharesOrder = oldScreenSharesOrder.filter( + participantId => knownSharingParticipantIds.includes(participantId)); + + // Make sure all new sharing participant get added to the end of the + // known screen shares. + knownSharingParticipantIds.forEach(participantId => { + if (!newScreenSharesOrder.includes(participantId)) { + newScreenSharesOrder.push(participantId); + } + }); + + if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) { + store.dispatch( + setParticipantsWithScreenShare(newScreenSharesOrder)); + + _updateAutoPinnedParticipant(store); + } + } +); + +/** + * Private helper to automatically pin the latest screen share stream or unpin + * if there are no more screen share streams. + * + * @param {Store} store - The redux store. + * @returns {void} + */ +function _updateAutoPinnedParticipant({ dispatch, getState }) { + const state = getState(); + const screenShares = state['features/video-layout'].screenShares; + + if (!screenShares) { + return; + } + + const latestScreenshareParticipantId + = screenShares[screenShares.length - 1]; + + if (latestScreenshareParticipantId) { + dispatch(pinParticipant(latestScreenshareParticipantId)); + } else if (getPinnedParticipant(state['features/base/participants'])) { + dispatch(pinParticipant(null)); + } +}