feat(remotecontrol): Prevent multiple remote control sessions (#1875)

This commit is contained in:
hristoterezov 2017-08-17 19:43:22 +03:00 committed by virtuacoplenny
parent 31dd3da2b6
commit 378a8d014e
10 changed files with 217 additions and 59 deletions

View File

@ -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

View File

@ -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,

View File

@ -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 <tt>true</tt> after the 'onplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
@ -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 <tt>id</tt> in the
* given <tt>parentElement</tt>.
*
* @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();
}
};

View File

@ -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()
);
}
};

View File

@ -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'

View File

@ -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

View File

@ -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;
});
}
/**

View File

@ -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;
}

View File

@ -99,9 +99,15 @@ class RemoteVideoMenuTriggerButton extends Component {
* @returns {ReactElement}
*/
render() {
const content = this._renderRemoteVideoMenu();
if (!content) {
return null;
}
return (
<AKInlineDialog
content = { this._renderRemoteVideoMenu() }
content = { content }
isOpen = { this.state.showRemoteMenu }
onClose = { this._onRemoteMenuClose }
position = { interfaceConfig.VERTICAL_FILMSTRIP
@ -162,32 +168,52 @@ class RemoteVideoMenuTriggerButton extends Component {
participantID
} = this.props;
return (
<RemoteVideoMenu id = { participantID }>
{ isModerator
? <MuteButton
isAudioMuted = { isAudioMuted }
onClick = { this._onRemoteMenuClose }
participantID = { participantID } />
: null }
{ isModerator
? <KickButton
onClick = { this._onRemoteMenuClose }
participantID = { participantID } />
: null }
{ remoteControlState
? <RemoteControlButton
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { remoteControlState } />
: null }
{ onVolumeChange
? <VolumeSlider
initialValue = { initialVolumeValue }
onChange = { onVolumeChange } />
: null }
</RemoteVideoMenu>
);
const buttons = [];
if (isModerator) {
buttons.push(
<MuteButton
isAudioMuted = { isAudioMuted }
key = 'mute'
onClick = { this._onRemoteMenuClose }
participantID = { participantID } />
);
buttons.push(
<KickButton
key = 'kick'
onClick = { this._onRemoteMenuClose }
participantID = { participantID } />
);
}
if (remoteControlState) {
buttons.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { remoteControlState } />
);
}
if (onVolumeChange && isModerator) {
buttons.push(
<VolumeSlider
initialValue = { initialVolumeValue }
key = 'volume-slider'
onChange = { onVolumeChange } />
);
}
if (buttons.length > 0) {
return (
<RemoteVideoMenu id = { participantID }>
{ buttons }
</RemoteVideoMenu>
);
}
return null;
}
}

View File

@ -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';