From 378a8d014e28842da678b4e6c125590e4e37a6a6 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Thu, 17 Aug 2017 19:43:22 +0300 Subject: [PATCH] feat(remotecontrol): Prevent multiple remote control sessions (#1875) --- conference.js | 22 ++++- modules/UI/UI.js | 20 +++++ modules/UI/videolayout/RemoteVideo.js | 55 +++++++------ modules/UI/videolayout/VideoLayout.js | 25 +++++- modules/remotecontrol/Controller.js | 19 ++++- modules/remotecontrol/Receiver.js | 5 ++ modules/remotecontrol/RemoteControl.js | 38 ++++++++- .../remotecontrol/RemoteControlParticipant.js | 4 +- .../RemoteVideoMenuTriggerButton.js | 80 ++++++++++++------- service/remotecontrol/RemoteControlEvents.js | 8 ++ 10 files changed, 217 insertions(+), 59 deletions(-) create mode 100644 service/remotecontrol/RemoteControlEvents.js diff --git a/conference.js b/conference.js index 533be995d7..cf1b003885 100644 --- a/conference.js +++ b/conference.js @@ -11,6 +11,8 @@ import mediaDeviceHelper from './modules/devices/mediaDeviceHelper'; import { reload, reportError } from './modules/util/helpers'; +import * as RemoteControlEvents + from './service/remotecontrol/RemoteControlEvents'; import UIEvents from './service/UI/UIEvents'; import UIUtil from './modules/UI/util/UIUtil'; import * as JitsiMeetConferenceEvents from './ConferenceEvents'; @@ -1813,10 +1815,27 @@ export default { ConferenceEvents.LOCK_STATE_CHANGED, (...args) => APP.store.dispatch(lockStateChanged(room, ...args))); + APP.remoteControl.on(RemoteControlEvents.ACTIVE_CHANGED, isActive => { + room.setLocalParticipantProperty( + "remoteControlSessionStatus", + isActive + ); + APP.UI.setLocalRemoteControlActiveChanged(); + }); + room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, (participant, name, oldValue, newValue) => { - if (name === "raisedHand") { + switch (name) { + case 'raisedHand': APP.UI.setRaisedHandStatus(participant, newValue); + break; + case 'remoteControlSessionStatus': + APP.UI.setRemoteControlActiveStatus( + participant.getId(), + newValue); + break; + default: + // ignore } }); @@ -2361,6 +2380,7 @@ export default { APP.UI.setLocalRaisedHandStatus(raisedHand); } }, + /** * Log event to callstats and analytics. * @param {string} name the event name diff --git a/modules/UI/UI.js b/modules/UI/UI.js index f172d7f3d2..70cdf022fd 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1230,6 +1230,26 @@ UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount(); UI.setRemoteThumbnailsVisibility = shouldHide => Filmstrip.setRemoteVideoVisibility(shouldHide); +/** + * Sets the remote control active status for a remote participant. + * + * @param {string} participantID - The id of the remote participant. + * @param {boolean} isActive - The new remote control active status. + * @returns {void} + */ +UI.setRemoteControlActiveStatus = function(participantID, isActive) { + VideoLayout.setRemoteControlActiveStatus(participantID, isActive); +}; + +/** + * Sets the remote control active status for the local participant. + * + * @returns {void} + */ +UI.setLocalRemoteControlActiveChanged = function() { + VideoLayout.setLocalRemoteControlActiveChanged(); +}; + const UIListeners = new Map([ [ UIEvents.ETHERPAD_CLICKED, diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 0aec85b8b9..d338d377b4 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -51,6 +51,7 @@ function RemoteVideo(user, VideoLayout, emitter) { this.flipX = false; this.isLocal = false; this.popupMenuIsHovered = false; + this._isRemoteControlSessionActive = false; /** * The flag is set to true after the 'onplay' event has been * triggered on the current video element. It goes back to false @@ -87,9 +88,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() { this.initBrowserSpecificProperties(); - if (APP.conference.isModerator || this._supportsRemoteControl) { - this.addRemoteVideoMenu(); - } + this.addRemoteVideoMenu(); this.VideoLayout.resizeThumbnails(false, true); @@ -135,7 +134,9 @@ RemoteVideo.prototype._generatePopupContent = function () { let remoteControlState = null; let onRemoteControlToggle; - if (this._supportsRemoteControl) { + if (this._supportsRemoteControl + && ((!APP.remoteControl.active && !this._isRemoteControlSessionActive) + || APP.remoteControl.controller.activeParticipant === this.id)) { if (controller.getRequestedParticipant() === this.id) { onRemoteControlToggle = () => {}; remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING; @@ -179,7 +180,18 @@ RemoteVideo.prototype._generatePopupContent = function () { }; RemoteVideo.prototype._onRemoteVideoMenuDisplay = function () { - this.updateRemoteVideoMenu(this.isAudioMuted, true); + this.updateRemoteVideoMenu(); +}; + +/** + * Sets the remote control active status for the remote video. + * + * @param {boolean} isActive - The new remote control active status. + * @returns {void} + */ +RemoteVideo.prototype.setRemoteControlActiveStatus = function(isActive) { + this._isRemoteControlSessionActive = isActive; + this.updateRemoteVideoMenu(); }; /** @@ -192,18 +204,7 @@ RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) { return; } this._supportsRemoteControl = isSupported; - if(!isSupported) { - return; - } - - if(!this.hasRemoteVideoMenu) { - //create menu - this.addRemoteVideoMenu(); - } else { - //update the content - this.updateRemoteVideoMenu(this.isAudioMuted, true); - } - + this.updateRemoteVideoMenu(); }; /** @@ -215,7 +216,7 @@ RemoteVideo.prototype._requestRemoteControlPermissions = function () { if(result === null) { return; } - this.updateRemoteVideoMenu(this.isAudioMuted, true); + this.updateRemoteVideoMenu(); APP.UI.messageHandler.notify( "dialog.remoteControlTitle", (result === false) ? "dialog.remoteControlDeniedMessage" @@ -232,7 +233,7 @@ RemoteVideo.prototype._requestRemoteControlPermissions = function () { } }, error => { logger.error(error); - this.updateRemoteVideoMenu(this.isAudioMuted, true); + this.updateRemoteVideoMenu(); APP.UI.messageHandler.notify( "dialog.remoteControlTitle", "dialog.remoteControlErrorMessage", @@ -240,7 +241,7 @@ RemoteVideo.prototype._requestRemoteControlPermissions = function () { || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} ); }); - this.updateRemoteVideoMenu(this.isAudioMuted, true); + this.updateRemoteVideoMenu(); }; /** @@ -249,7 +250,7 @@ RemoteVideo.prototype._requestRemoteControlPermissions = function () { RemoteVideo.prototype._stopRemoteControl = function () { // send message about stopping APP.remoteControl.controller.stop(); - this.updateRemoteVideoMenu(this.isAudioMuted, true); + this.updateRemoteVideoMenu(); }; /** @@ -286,9 +287,10 @@ RemoteVideo.prototype._setAudioVolume = function (newVal) { * Updates the remote video menu. * * @param isMuted the new muted state to update to - * @param force to work even if popover is not visible */ -RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) { +RemoteVideo.prototype.updateRemoteVideoMenu = function ( + isMuted = this.isAudioMuted +) { this.isAudioMuted = isMuted; this._generatePopupContent(); @@ -323,8 +325,6 @@ RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() { * Adds the remote video menu element for the given id in the * given parentElement. * - * @param id the id indicating the video for which we're adding a menu. - * @param parentElement the parent element where this menu will be added */ RemoteVideo.prototype.addRemoteVideoMenu = function () { if (interfaceConfig.filmStripOnly) { @@ -576,6 +576,11 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) { if (!isVideo) { this._audioStreamElement = streamElement; + + // If the remote video menu was created before the audio stream was + // attached we need to update the menu in order to show the volume + // slider. + this.updateRemoteVideoMenu(); } }; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 4b7e289deb..9baafc3a74 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -642,9 +642,7 @@ var VideoLayout = { return; remoteVideo.showAudioIndicator(isMuted); - if (APP.conference.isModerator) { - remoteVideo.updateRemoteVideoMenu(isMuted); - } + remoteVideo.updateRemoteVideoMenu(isMuted); } }, @@ -1165,6 +1163,27 @@ var VideoLayout = { */ getRemoteVideosCount() { return Object.keys(remoteVideos).length; + }, + /** + * Sets the remote control active status for a remote participant. + * + * @param {string} participantID - The id of the remote participant. + * @param {boolean} isActive - The new remote control active status. + * @returns {void} + */ + setRemoteControlActiveStatus(participantID, isActive) { + remoteVideos[participantID].setRemoteControlActiveStatus(isActive); + }, + + /** + * Sets the remote control active status for the local participant. + * + * @returns {void} + */ + setLocalRemoteControlActiveChanged() { + Object.values(remoteVideos).forEach( + remoteVideo => remoteVideo.updateRemoteVideoMenu() + ); } }; diff --git a/modules/remotecontrol/Controller.js b/modules/remotecontrol/Controller.js index f53f01b6d0..11f4f21421 100644 --- a/modules/remotecontrol/Controller.js +++ b/modules/remotecontrol/Controller.js @@ -8,6 +8,8 @@ import { PERMISSIONS_ACTIONS, REMOTE_CONTROL_MESSAGE_NAME } from '../../service/remotecontrol/Constants'; +import * as RemoteControlEvents + from '../../service/remotecontrol/RemoteControlEvents'; import UIEvents from '../../service/UI/UIEvents'; import RemoteControlParticipant from './RemoteControlParticipant'; @@ -86,6 +88,15 @@ export default class Controller extends RemoteControlParticipant { = this._onLargeVideoIdChanged.bind(this); } + /** + * Returns the current active participant's id. + * + * @returns {string|null} - The id of the current active participant. + */ + get activeParticipant(): string | null { + return this._requestedParticipant || this._controlledParticipant; + } + /** * Requests permissions from the remote control receiver side. * @@ -100,7 +111,7 @@ export default class Controller extends RemoteControlParticipant { if (!this._enabled) { return Promise.reject(new Error('Remote control is disabled!')); } - + this.emit(RemoteControlEvents.ACTIVE_CHANGED, true); this._area = eventCaptureArea;// $("#largeVideoWrapper") logger.log(`Requsting remote control permissions from: ${userId}`); @@ -125,16 +136,21 @@ export default class Controller extends RemoteControlParticipant { result = this._handleReply(participant, event); } catch (e) { clearRequest(); + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); reject(e); } if (result !== null) { clearRequest(); + if (result === false) { + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); + } resolve(result); } }; onUserLeft = id => { if (id === this._requestedParticipant) { clearRequest(); + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); resolve(null); } }; @@ -316,6 +332,7 @@ export default class Controller extends RemoteControlParticipant { this.pause(); this._controlledParticipant = null; this._area = undefined; + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); APP.UI.messageHandler.notify( 'dialog.remoteControlTitle', 'dialog.remoteControlStopMessage' diff --git a/modules/remotecontrol/Receiver.js b/modules/remotecontrol/Receiver.js index a03cfe5c9f..3bbf20c57a 100644 --- a/modules/remotecontrol/Receiver.js +++ b/modules/remotecontrol/Receiver.js @@ -13,6 +13,8 @@ import { REMOTE_CONTROL_MESSAGE_NAME, REQUESTS } from '../../service/remotecontrol/Constants'; +import * as RemoteControlEvents + from '../../service/remotecontrol/RemoteControlEvents'; import { Transport, PostMessageTransportBackend @@ -132,6 +134,7 @@ export default class Receiver extends RemoteControlParticipant { name: REMOTE_CONTROL_MESSAGE_NAME, type: EVENTS.stop }); + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); if (!dontNotify) { APP.UI.messageHandler.notify( 'dialog.remoteControlTitle', @@ -177,6 +180,7 @@ export default class Receiver extends RemoteControlParticipant { && message.action === PERMISSIONS_ACTIONS.request) { const userId = participant.getId(); + this.emit(RemoteControlEvents.ACTIVE_CHANGED, true); APP.store.dispatch( openRemoteControlAuthorizationDialog(userId)); } else if (this._controller === participant.getId()) { @@ -200,6 +204,7 @@ export default class Receiver extends RemoteControlParticipant { * @returns {void} */ deny(userId: string) { + this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); this.sendRemoteControlEndpointMessage(userId, { type: EVENTS.permissions, action: PERMISSIONS_ACTIONS.deny diff --git a/modules/remotecontrol/RemoteControl.js b/modules/remotecontrol/RemoteControl.js index c4a393e563..c504e53670 100644 --- a/modules/remotecontrol/RemoteControl.js +++ b/modules/remotecontrol/RemoteControl.js @@ -1,9 +1,12 @@ /* @flow */ +import EventEmitter from 'events'; import { getLogger } from 'jitsi-meet-logger'; import { DISCO_REMOTE_CONTROL_FEATURE } from '../../service/remotecontrol/Constants'; +import * as RemoteControlEvents + from '../../service/remotecontrol/RemoteControlEvents'; import Controller from './Controller'; import Receiver from './Receiver'; @@ -16,7 +19,8 @@ declare var config: Object; /** * Implements the remote control functionality. */ -class RemoteControl { +class RemoteControl extends EventEmitter { + _active: boolean; _initialized: boolean; controller: Controller; receiver: Receiver; @@ -25,8 +29,36 @@ class RemoteControl { * Constructs new instance. Creates controller and receiver properties. */ constructor() { + super(); this.controller = new Controller(); + this._active = false; this._initialized = false; + + this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => { + this.active = active; + }); + } + + /** + * Sets the remote control session active status. + * + * @param {boolean} isActive - True - if the controller or the receiver is + * currently in remote control session and false otherwise. + * @returns {void} + */ + set active(isActive: boolean) { + this._active = isActive; + this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive); + } + + /** + * Returns the remote control session active status. + * + * @returns {boolean} - True - if the controller or the receiver is + * currently in remote control session and false otherwise. + */ + get active(): boolean { + return this._active; } /** @@ -45,6 +77,10 @@ class RemoteControl { this._initialized = true; this.controller.enable(true); this.receiver = new Receiver(); + + this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => { + this.active = active; + }); } /** diff --git a/modules/remotecontrol/RemoteControlParticipant.js b/modules/remotecontrol/RemoteControlParticipant.js index d85c36080a..39a2f3a0e0 100644 --- a/modules/remotecontrol/RemoteControlParticipant.js +++ b/modules/remotecontrol/RemoteControlParticipant.js @@ -1,5 +1,6 @@ /* @flow */ +import EventEmitter from 'events'; import { getLogger } from 'jitsi-meet-logger'; import { @@ -13,13 +14,14 @@ declare var APP: Object; /** * Implements common logic for Receiver class and Controller class. */ -export default class RemoteControlParticipant { +export default class RemoteControlParticipant extends EventEmitter { _enabled: boolean; /** * Creates new instance. */ constructor() { + super(); this._enabled = false; } diff --git a/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js index 99f11b593e..e27120d26e 100644 --- a/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js +++ b/react/features/remote-video-menu/components/RemoteVideoMenuTriggerButton.js @@ -99,9 +99,15 @@ class RemoteVideoMenuTriggerButton extends Component { * @returns {ReactElement} */ render() { + const content = this._renderRemoteVideoMenu(); + + if (!content) { + return null; + } + return ( - { isModerator - ? - : null } - { isModerator - ? - : null } - { remoteControlState - ? - : null } - { onVolumeChange - ? - : null } - - ); + const buttons = []; + + if (isModerator) { + buttons.push( + + ); + buttons.push( + + ); + } + + if (remoteControlState) { + buttons.push( + + ); + } + + if (onVolumeChange && isModerator) { + buttons.push( + + ); + } + + if (buttons.length > 0) { + return ( + + { buttons } + + ); + } + + return null; } } diff --git a/service/remotecontrol/RemoteControlEvents.js b/service/remotecontrol/RemoteControlEvents.js new file mode 100644 index 0000000000..3333ecdd17 --- /dev/null +++ b/service/remotecontrol/RemoteControlEvents.js @@ -0,0 +1,8 @@ +/** + * Events fired from the remote control module through the EventEmitter. + */ + +/** + * Notifies about remote control active session status changes. + */ +export const ACTIVE_CHANGED = 'active-changed';