455 lines
15 KiB
JavaScript
455 lines
15 KiB
JavaScript
/* @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 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);
|
|
}
|
|
|
|
/**
|
|
* 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<boolean>} 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._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();
|
|
reject(e);
|
|
}
|
|
if (result !== null) {
|
|
clearRequest();
|
|
resolve(result);
|
|
}
|
|
};
|
|
onUserLeft = id => {
|
|
if (id === this._requestedParticipant) {
|
|
clearRequest();
|
|
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 => {
|
|
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
|
type: EVENTS.mousescroll,
|
|
x: event.deltaX,
|
|
y: event.deltaY
|
|
});
|
|
};
|
|
$(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;
|
|
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();
|
|
}
|
|
}
|
|
}
|