/* @flow */ import { getLogger } from 'jitsi-meet-logger'; import * as KeyCodes from '../keycode/keycode'; import { EVENTS, 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'; declare var $: Function; declare var APP: Object; declare var JitsiMeetJS: Object; const ConferenceEvents = JitsiMeetJS.events.conference; const logger = getLogger(__filename); /** * Extract the keyboard key from the keyboard event. * * @param {KeyboardEvent} event - The event. * @returns {KEYS} The key that is pressed or undefined. */ function getKey(event) { return KeyCodes.keyboardEventToKey(event); } /** * Extract the modifiers from the keyboard event. * * @param {KeyboardEvent} event - The event. * @returns {Array} With possible values: "shift", "control", "alt", "command". */ function getModifiers(event) { const modifiers = []; if (event.shiftKey) { modifiers.push('shift'); } if (event.ctrlKey) { modifiers.push('control'); } if (event.altKey) { modifiers.push('alt'); } if (event.metaKey) { modifiers.push('command'); } return modifiers; } /** * This class represents the controller party for a remote controller session. * It listens for mouse and keyboard events and sends them to the receiver * party of the remote control session. */ export default class Controller extends RemoteControlParticipant { _area: ?Object; _controlledParticipant: string | null; _isCollectingEvents: boolean; _largeVideoChangedListener: Function; _requestedParticipant: string | null; _stopListener: Function; _userLeftListener: Function; /** * Creates new instance. */ constructor() { super(); this._isCollectingEvents = false; this._controlledParticipant = null; this._requestedParticipant = null; this._stopListener = this._handleRemoteControlStoppedEvent.bind(this); this._userLeftListener = this._onUserLeft.bind(this); this._largeVideoChangedListener = 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. * * @param {string} userId - The user id of the participant that will be * requested. * @param {JQuerySelector} eventCaptureArea - The area that is going to be * used mouse and keyboard event capture. * @returns {Promise} Resolve values - true(accept), false(deny), * null(the participant has left). */ requestPermissions(userId: string, eventCaptureArea: Object) { 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}`); return new Promise((resolve, reject) => { // eslint-disable-next-line prefer-const let onUserLeft, permissionsReplyListener; const clearRequest = () => { this._requestedParticipant = null; APP.conference.removeConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener); APP.conference.removeConferenceListener( ConferenceEvents.USER_LEFT, onUserLeft); }; permissionsReplyListener = (participant, event) => { let result = null; try { 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); } }; APP.conference.addConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener); APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, onUserLeft); this._requestedParticipant = userId; this.sendRemoteControlEndpointMessage(userId, { type: EVENTS.permissions, action: PERMISSIONS_ACTIONS.request }, e => { clearRequest(); reject(e); }); }); } /** * Handles the reply of the permissions request. * * @param {JitsiParticipant} participant - The participant that has sent the * reply. * @param {RemoteControlEvent} event - The remote control event. * @returns {void} */ _handleReply(participant: Object, event: Object) { const userId = participant.getId(); if (this._enabled && event.name === REMOTE_CONTROL_MESSAGE_NAME && event.type === EVENTS.permissions && userId === this._requestedParticipant) { if (event.action !== PERMISSIONS_ACTIONS.grant) { this._area = undefined; } switch (event.action) { case PERMISSIONS_ACTIONS.grant: { this._controlledParticipant = userId; logger.log('Remote control permissions granted to:', userId); this._start(); return true; } case PERMISSIONS_ACTIONS.deny: return false; case PERMISSIONS_ACTIONS.error: throw new Error('Error occurred on receiver side'); default: 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 - EndpointMessage event from the data channels. * @property {string} type - The function process only events with * name REMOTE_CONTROL_MESSAGE_NAME. * @returns {void} */ _handleRemoteControlStoppedEvent(participant: Object, event: Object) { if (this._enabled && event.name === REMOTE_CONTROL_MESSAGE_NAME && event.type === EVENTS.stop && participant.getId() === this._controlledParticipant) { this._stop(); } } /** * Starts processing the mouse and keyboard events. Sets conference * listeners. Disables keyboard events. * * @returns {void} */ _start() { logger.log('Starting remote control controller.'); APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED, this._largeVideoChangedListener); APP.conference.addConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._stopListener); APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, this._userLeftListener); this.resume(); } /** * Disables the keyboatd shortcuts. Starts collecting remote control * events. It can be used to resume an active remote control session wchich * was paused with this.pause(). * * @returns {void} */ resume() { if (!this._enabled || this._isCollectingEvents || !this._area) { return; } logger.log('Resuming remote control controller.'); this._isCollectingEvents = true; APP.keyboardshortcut.enable(false); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.mousemove(event => { // $FlowDisableNextLine: we are sure that this._area is not null. const position = this._area.position(); this.sendRemoteControlEndpointMessage(this._controlledParticipant, { type: EVENTS.mousemove, // $FlowDisableNextLine: we are sure that this._area is not null x: (event.pageX - position.left) / this._area.width(), // $FlowDisableNextLine: we are sure that this._area is not null y: (event.pageY - position.top) / this._area.height() }); }); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.mousedown(this._onMouseClickHandler.bind(this, EVENTS.mousedown)); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.mouseup(this._onMouseClickHandler.bind(this, EVENTS.mouseup)); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.dblclick( this._onMouseClickHandler.bind(this, EVENTS.mousedblclick)); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.contextmenu(() => false); // $FlowDisableNextLine: we are sure that this._area is not null. this._area[0].onmousewheel = event => { event.preventDefault(); event.stopPropagation(); this.sendRemoteControlEndpointMessage(this._controlledParticipant, { type: EVENTS.mousescroll, x: event.deltaX, y: event.deltaY }); return false; }; $(window).keydown(this._onKeyPessHandler.bind(this, EVENTS.keydown)); $(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup)); } /** * Stops processing the mouse and keyboard events. Removes added listeners. * Enables the keyboard shortcuts. Displays dialog to notify the user that * remote control session has ended. * * @returns {void} */ _stop() { if (!this._controlledParticipant) { return; } logger.log('Stopping remote control controller.'); APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED, this._largeVideoChangedListener); APP.conference.removeConferenceListener( ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._stopListener); APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT, this._userLeftListener); this.pause(); this._controlledParticipant = null; this._area = undefined; this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); APP.UI.messageHandler.notify( 'dialog.remoteControlTitle', 'dialog.remoteControlStopMessage' ); } /** * Executes this._stop() mehtod which stops processing the mouse and * keyboard events, removes added listeners, enables the keyboard shortcuts, * displays dialog to notify the user that remote control session has ended. * In addition sends stop message to the controlled participant. * * @returns {void} */ stop() { if (!this._controlledParticipant) { return; } this.sendRemoteControlEndpointMessage(this._controlledParticipant, { type: EVENTS.stop }); this._stop(); } /** * Pauses the collecting of events and enables the keyboard shortcus. But * it doesn't removes any other listeners. Basically the remote control * session will be still active after this.pause(), but no events from the * controller side will be captured and sent. You can resume the collecting * of the events with this.resume(). * * @returns {void} */ pause() { if (!this._controlledParticipant) { return; } logger.log('Pausing remote control controller.'); this._isCollectingEvents = false; APP.keyboardshortcut.enable(true); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.off('mousemove'); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.off('mousedown'); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.off('mouseup'); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.off('contextmenu'); // $FlowDisableNextLine: we are sure that this._area is not null. this._area.off('dblclick'); $(window).off('keydown'); $(window).off('keyup'); // $FlowDisableNextLine: we are sure that this._area is not null. this._area[0].onmousewheel = undefined; } /** * Handler for mouse click events. * * @param {string} type - The type of event ("mousedown"/"mouseup"). * @param {Event} event - The mouse event. * @returns {void} */ _onMouseClickHandler(type: string, event: Object) { this.sendRemoteControlEndpointMessage(this._controlledParticipant, { type, button: event.which }); } /** * Returns true if the remote control session is started. * * @returns {boolean} */ isStarted() { return this._controlledParticipant !== null; } /** * Returns the id of the requested participant. * * @returns {string} The id of the requested participant. * NOTE: This id should be the result of JitsiParticipant.getId() call. */ getRequestedParticipant() { return this._requestedParticipant; } /** * Handler for key press events. * * @param {string} type - The type of event ("keydown"/"keyup"). * @param {Event} event - The key event. * @returns {void} */ _onKeyPessHandler(type: string, event: Object) { this.sendRemoteControlEndpointMessage(this._controlledParticipant, { type, key: getKey(event), modifiers: getModifiers(event) }); } /** * Calls the stop method if the other side have left. * * @param {string} id - The user id for the participant that have left. * @returns {void} */ _onUserLeft(id: string) { if (this._controlledParticipant === id) { this._stop(); } } /** * Handles changes of the participant displayed on the large video. * * @param {string} id - The user id for the participant that is displayed. * @returns {void} */ _onLargeVideoIdChanged(id: string) { if (!this._controlledParticipant) { return; } if (this._controlledParticipant === id) { this.resume(); } else { this.pause(); } } }