diff --git a/conference.js b/conference.js index a51192de9..36309b9cd 100644 --- a/conference.js +++ b/conference.js @@ -985,8 +985,6 @@ export default { let externalInstallation = false; if (shareScreen) { - // For remote control testing: - // remoteControlReceiver.start(); createLocalTracks({ devices: ['desktop'], desktopSharingExtensionExternalInstallation: { @@ -1076,8 +1074,7 @@ export default { dialogTitleKey, dialogTxt, false); }); } else { - // For remote control testing: - // remoteControlReceiver.stop(); + APP.remoteControl.receiver.stop(); createLocalTracks({ devices: ['video'] }).then( ([stream]) => this.useVideoStream(stream) ).then(() => { diff --git a/modules/API/API.js b/modules/API/API.js index b9860d09b..471ccfe48 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -56,8 +56,8 @@ function initCommands() { "video-hangup": () => APP.conference.hangup(), "email": APP.conference.changeLocalEmail, "avatar-url": APP.conference.changeLocalAvatarUrl, - "remote-control-supported": isSupported => - APP.remoteControl.onRemoteControlSupported(isSupported) + "remote-control-event": event => + APP.remoteControl.onRemoteControlAPIEvent(event) }; Object.keys(commands).forEach(function (key) { postis.listen(key, args => commands[key](...args)); diff --git a/modules/remotecontrol/Controller.js b/modules/remotecontrol/Controller.js index 56b739f11..6e60e1189 100644 --- a/modules/remotecontrol/Controller.js +++ b/modules/remotecontrol/Controller.js @@ -1,7 +1,10 @@ -/* global $, APP */ +/* global $, JitsiMeetJS, APP */ import * as KeyCodes from "../keycode/keycode"; -import {EVENT_TYPES, API_EVENT_TYPE} +import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants"; +import RemoteControlParticipant from "./RemoteControlParticipant"; + +const ConferenceEvents = JitsiMeetJS.events.conference; /** * Extract the keyboard key from the keyboard event. @@ -44,34 +47,113 @@ function getModifiers(event) { * It listens for mouse and keyboard events and sends them to the receiver * party of the remote control session. */ -export default class Controller { +export default class Controller extends RemoteControlParticipant { /** * Creates new instance. */ constructor() { - this.enabled = false; + super(); + this.controlledParticipant = null; + this.requestedParticipant = null; + this.stopListener = this._handleRemoteControlStoppedEvent.bind(this); } /** - * Enables / Disables the remote control - * @param {boolean} enabled the new state. + * Requests permissions from the remote control receiver side. + * @param {string} userId the user id of the participant that will be + * requested. */ - enable(enabled) { - this.enabled = enabled; + requestPermissions(userId) { + if(!this.enabled) { + return Promise.reject(new Error("Remote control is disabled!")); + } + return new Promise((resolve, reject) => { + let permissionsReplyListener = (participant, event) => { + let result = null; + try { + result = this._handleReply(participant, event); + } catch (e) { + reject(e); + } + if(result !== null) { + this.requestedParticipant = null; + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + permissionsReplyListener); + resolve(result); + } + }; + APP.conference.addConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + permissionsReplyListener); + this.requestedParticipant = userId; + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: PERMISSIONS_ACTIONS.request + }, e => { + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + permissionsReplyListener); + this.requestedParticipant = null; + reject(e); + }); + }); + } + + /** + * Handles the reply of the permissions request. + * @param {JitsiParticipant} participant the participant that has sent the + * reply + * @param {object} event the remote control event. + */ + _handleReply(participant, event) { + const remoteControlEvent = event.event; + const userId = participant.getId(); + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE + && remoteControlEvent.type === EVENT_TYPES.permissions + && userId === this.requestedParticipant) { + if(remoteControlEvent.action === PERMISSIONS_ACTIONS.grant) { + this.controlledParticipant = userId; + this._start(); + return true; + } else if(remoteControlEvent.action === PERMISSIONS_ACTIONS.deny) { + return false; + } else { + throw new Error("Unknown reply received!"); + } + } else { + //different message type or another user -> ignoring the message + return null; + } + } + + /** + * Handles remote control stopped. + * @param {JitsiParticipant} participant the participant that has sent the + * event + * @param {object} event the the remote control event. + */ + _handleRemoteControlStoppedEvent(participant, event) { + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE + && event.event.type === EVENT_TYPES.stop + && participant.getId() === this.controlledParticipant) { + this._stop(); + } } /** * Starts processing the mouse and keyboard events. - * @param {JQuery.selector} area the selector which will be used for - * attaching the listeners on. */ - start(area) { + _start() { if(!this.enabled) return; - this.area = area; + APP.conference.addConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this.stopListener); + this.area = $("#largeVideoWrapper"); this.area.mousemove(event => { const position = this.area.position(); - this._sendRemoteControlEvent({ + this._sendRemoteControlEvent(this.controlledParticipant, { type: EVENT_TYPES.mousemove, x: (event.pageX - position.left)/this.area.width(), y: (event.pageY - position.top)/this.area.height() @@ -85,7 +167,7 @@ export default class Controller { this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick)); this.area.contextmenu(() => false); this.area[0].onmousewheel = event => { - this._sendRemoteControlEvent({ + this._sendRemoteControlEvent(this.controlledParticipant, { type: EVENT_TYPES.mousescroll, x: event.deltaX, y: event.deltaY @@ -99,7 +181,11 @@ export default class Controller { /** * Stops processing the mouse and keyboard events. */ - stop() { + _stop() { + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this.stopListener); + this.controlledParticipant = null; this.area.off( "mousemove" ); this.area.off( "mousedown" ); this.area.off( "mouseup" ); @@ -116,7 +202,7 @@ export default class Controller { * @param {Event} event the mouse event. */ _onMouseClickHandler(type, event) { - this._sendRemoteControlEvent({ + this._sendRemoteControlEvent(this.controlledParticipant, { type: type, button: event.which }); @@ -128,25 +214,10 @@ export default class Controller { * @param {Event} event the key event. */ _onKeyPessHandler(type, event) { - this._sendRemoteControlEvent({ + this._sendRemoteControlEvent(this.controlledParticipant, { type: type, key: getKey(event), modifiers: getModifiers(event), }); } - - /** - * Sends remote control event to the controlled participant. - * @param {Object} event the remote control event. - */ - _sendRemoteControlEvent(event) { - if(!this.enabled) - return; - try{ - APP.conference.sendEndpointMessage("", - {type: API_EVENT_TYPE, event}); - } catch (e) { - // failed to send the event. - } - } } diff --git a/modules/remotecontrol/Receiver.js b/modules/remotecontrol/Receiver.js index 7785d6324..e23017b7e 100644 --- a/modules/remotecontrol/Receiver.js +++ b/modules/remotecontrol/Receiver.js @@ -1,6 +1,7 @@ /* global APP, JitsiMeetJS */ -import {DISCO_REMOTE_CONTROL_FEATURE, API_EVENT_TYPE} - from "../../service/remotecontrol/Constants"; +import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES, + PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants"; +import RemoteControlParticipant from "./RemoteControlParticipant"; const ConferenceEvents = JitsiMeetJS.events.conference; @@ -10,13 +11,16 @@ const ConferenceEvents = JitsiMeetJS.events.conference; * API module. From there the events can be received from wrapper application * and executed. */ -export default class Receiver { +export default class Receiver extends RemoteControlParticipant { /** * Creates new instance. * @constructor */ constructor() { - this.enabled = false; + super(); + this.controller = null; + this._remoteControlEventsListener + = this._onRemoteControlEvent.bind(this); } /** @@ -28,17 +32,9 @@ export default class Receiver { this.enabled = enabled; // Announce remote control support. APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true); - } - } - - /** - * Attaches listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED events. - */ - start() { - if(this.enabled) { APP.conference.addConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, - this._onRemoteControlEvent); + this._remoteControlEventsListener); } } @@ -49,7 +45,13 @@ export default class Receiver { stop() { APP.conference.removeConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, - this._onRemoteControlEvent); + this._remoteControlEventsListener); + const event = { + type: EVENT_TYPES.stop + }; + this._sendRemoteControlEvent(this.controller, event); + this.controller = null; + APP.API.sendRemoteControlEvent(event); } /** @@ -58,7 +60,34 @@ export default class Receiver { * @param {Object} event the remote control event. */ _onRemoteControlEvent(participant, event) { - if(event.type === API_EVENT_TYPE && this.enabled) - APP.API.sendRemoteControlEvent(event.event); + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE) { + const remoteControlEvent = event.event; + if(this.controller === null + && remoteControlEvent.type === EVENT_TYPES.permissions + && remoteControlEvent.action === PERMISSIONS_ACTIONS.request) { + remoteControlEvent.userId = participant.getId(); + remoteControlEvent.userJID = participant.getJid(); + remoteControlEvent.displayName = participant.getDisplayName(); + } else if(this.controller !== participant.getId()) { + return; + } + APP.API.sendRemoteControlEvent(remoteControlEvent); + } + } + + /** + * Handles remote control permission events received from the API module. + * @param {String} userId the user id of the participant related to the + * event. + * @param {PERMISSIONS_ACTIONS} action the action related to the event. + */ + _onRemoteControlPermissionsEvent(userId, action) { + if(action === PERMISSIONS_ACTIONS.grant) { + this.controller = userId; + } + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: action + }); } } diff --git a/modules/remotecontrol/RemoteControl.js b/modules/remotecontrol/RemoteControl.js index 2da4680dd..76d06a8ec 100644 --- a/modules/remotecontrol/RemoteControl.js +++ b/modules/remotecontrol/RemoteControl.js @@ -1,6 +1,8 @@ /* global APP, config */ import Controller from "./Controller"; import Receiver from "./Receiver"; +import {EVENT_TYPES} + from "../../service/remotecontrol/Constants"; /** * Implements the remote control functionality. @@ -32,14 +34,28 @@ class RemoteControl { this.controller.enable(true); } + /** + * Handles remote control events from the API module. + * @param {object} event the remote control event + */ + onRemoteControlAPIEvent(event) { + switch(event.type) { + case EVENT_TYPES.supported: + this._onRemoteControlSupported(); + break; + case EVENT_TYPES.permissions: + this.receiver._onRemoteControlPermissionsEvent( + event.userId, event.action); + break; + } + } + /** * Handles API event for support for executing remote control events into * the wrapper application. - * @param {boolean} isSupported true if the receiver side is supported by - * the wrapper application. */ - onRemoteControlSupported(isSupported) { - if(isSupported && !config.disableRemoteControl) { + _onRemoteControlSupported() { + if(!config.disableRemoteControl) { this.enabled = true; if(this.initialized) { this.receiver.enable(true); diff --git a/modules/remotecontrol/RemoteControlParticipant.js b/modules/remotecontrol/RemoteControlParticipant.js new file mode 100644 index 000000000..75323d7f6 --- /dev/null +++ b/modules/remotecontrol/RemoteControlParticipant.js @@ -0,0 +1,36 @@ +/* global APP */ +import {REMOTE_CONTROL_EVENT_TYPE} + from "../../service/remotecontrol/Constants"; + +export default class RemoteControlParticipant { + /** + * Creates new instance. + */ + constructor() { + this.enabled = false; + } + + /** + * Enables / Disables the remote control + * @param {boolean} enabled the new state. + */ + enable(enabled) { + this.enabled = enabled; + } + + /** + * Sends remote control event to other participant trough data channel. + * @param {Object} event the remote control event. + * @param {Function} onDataChannelFail handler for data channel failure. + */ + _sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) { + if(!this.enabled || !to) + return; + try{ + APP.conference.sendEndpointMessage(to, + {type: REMOTE_CONTROL_EVENT_TYPE, event}); + } catch (e) { + onDataChannelFail(e); + } + } +} diff --git a/service/remotecontrol/Constants.js b/service/remotecontrol/Constants.js index 34e776ea9..810e57f11 100644 --- a/service/remotecontrol/Constants.js +++ b/service/remotecontrol/Constants.js @@ -14,10 +14,22 @@ export const EVENT_TYPES = { mousedblclick: "mousedblclick", mousescroll: "mousescroll", keydown: "keydown", - keyup: "keyup" + keyup: "keyup", + permissions: "permissions", + stop: "stop", + supported: "supported" +}; + +/** + * Actions for the remote control permission events. + */ +export const PERMISSIONS_ACTIONS = { + request: "request", + grant: "grant", + deny: "deny" }; /** * The type of remote control events sent trough the API module. */ -export const API_EVENT_TYPE = "remote-control-event"; +export const REMOTE_CONTROL_EVENT_TYPE = "remote-control-event";