@jitsi/electron-sdk
Version:
Utilities for jitsi-meet-electron project
237 lines (220 loc) • 7.43 kB
JavaScript
const { ipcRenderer } = require('electron');
const os = require('os');
const postis = require("postis");
const constants = require("./constants");
const robot = require("@jitsi/robotjs");
const {
EVENTS,
KEY_ACTIONS_FROM_EVENT_TYPE,
MOUSE_ACTIONS_FROM_EVENT_TYPE,
MOUSE_BUTTONS,
REMOTE_CONTROL_MESSAGE_NAME,
REQUESTS
} = constants;
/**
* Parses the remote control events and executes them via robotjs.
* {@link RemoteControlMain} needs to be initialized in the main process.
* to work.
*/
class RemoteControl {
/**
* Constructs new instance and initializes the remote control functionality.
*
* @param {HTMLElement} iframe the Jitsi Meet iframe.
*/
constructor(iframe) {
this._iframe = iframe;
this._iframe.addEventListener('load', () => this._onIFrameLoad());
/**
* The status ("up"/"down") of the mouse button.
* FIXME: Assuming that one button at a time can be pressed. Haven't
* noticed any issues but maybe we should store the status for every
* mouse button that we are processing.
*/
this._mouseButtonStatus = "up";
}
/**
* Disposes the remote control functionality.
*/
dispose() {
if(this._channel) {
this._channel.destroy();
this._channel = null;
}
this._stop();
}
/**
* Returns the scale factor for the current display used to calculate the resolution of the display.
*
* NOTE: On Mac OS this._display.scaleFactor will always be 2 for some reason. But the values returned from
* this._display.bounds will already take into account the scale factor. That's why we are returning 1 for Mac OS.
*
* @returns {number} The scale factor.
*/
_getDisplayScaleFactor() {
return os.type() === 'Darwin' ? 1 : this._display.scaleFactor || 1;
}
/**
* Sets the display metrics(x, y, width, height, scaleFactor, etc...) of the display that will be used for the
* remote control.
*
* @param {string} sourceId - The source id of the desktop sharing stream.
* @returns {void}
*/
_setDisplayMetrics(sourceId) {
this._display = ipcRenderer.sendSync('jitsi-remotecontrol-get-display', sourceId);
}
/**
* Handles remote control start messages.
*
* @param {number} id - the id of the request that will be used for the
* response.
* @param {string} sourceId - The source id of the desktop sharing stream.
*/
_start(id, sourceId) {
this._displayMetricsChangeListener = () => {
this._setDisplayMetrics(sourceId);
};
ipcRenderer.on('jitsi-remotecontrol-displays-changed', this._displayMetricsChangeListener);
this._setDisplayMetrics(sourceId);
const response = {
id,
type: 'response'
};
if(this._display) {
response.result = true;
} else {
response.error
= 'Error: Can\'t detect the display that is currently shared';
}
this._sendMessage(response);
}
/**
* Stops processing the events.
*/
_stop() {
this._display = undefined;
if (this._displayMetricsChangeListener) {
ipcRenderer.removeListener('jitsi-remotecontrol-displays-changed', this._displayMetricsChangeListener);
this._displayMetricsChangeListener = undefined;
}
}
/**
* Handles iframe load events.
*/
_onIFrameLoad() {
this._iframe.contentWindow.addEventListener(
'unload',
() => this.dispose()
);
this._channel = postis({
window: this._iframe.contentWindow,
windowForEventListening: window,
scope: 'jitsi-remote-control'
});
this._channel.ready(() => {
this._channel.listen('message', message => {
const { name } = message.data;
if(name === REMOTE_CONTROL_MESSAGE_NAME) {
this._onRemoteControlMessage(message);
}
});
this._sendEvent({ type: EVENTS.supported });
});
}
/**
* Executes the passed message.
* @param {Object} message the remote control message.
*/
_onRemoteControlMessage(message) {
const { id, data } = message;
// If we haven't set the display prop. We haven't received the remote
// control start message or there was an error associating a display.
if(!this._display
&& data.type != REQUESTS.start) {
return;
}
switch(data.type) {
case EVENTS.mousemove: {
const { width, height, x, y } = this._display.bounds;
const scaleFactor = this._getDisplayScaleFactor();
const destX = data.x * width * scaleFactor + x;
const destY = data.y * height * scaleFactor + y;
if(this._mouseButtonStatus === "down") {
robot.dragMouse(destX, destY);
} else {
robot.moveMouse(destX, destY);
}
break;
}
case EVENTS.mousedown:
case EVENTS.mouseup: {
this._mouseButtonStatus
= MOUSE_ACTIONS_FROM_EVENT_TYPE[data.type];
robot.mouseToggle(
this._mouseButtonStatus,
(data.button
? MOUSE_BUTTONS[data.button] : undefined));
break;
}
case EVENTS.mousedblclick: {
robot.mouseClick(
(data.button
? MOUSE_BUTTONS[data.button] : undefined),
true);
break;
}
case EVENTS.mousescroll:{
const { x, y } = data;
if(x !== 0 || y !== 0) {
robot.scrollMouse(x, y);
}
break;
}
case EVENTS.keydown:
case EVENTS.keyup: {
if (data.key) {
robot.keyToggle(
data.key === 'caps_lock' ? 'capslock' : data.key,
KEY_ACTIONS_FROM_EVENT_TYPE[data.type],
data.modifiers);
}
break;
}
case REQUESTS.start: {
this._start(id, data.sourceId);
break;
}
case EVENTS.stop: {
this._stop();
break;
}
default:
console.error("Unknown event type!");
}
}
/**
* Sends remote control event to the controlled participant.
*
* @param {Object} event the remote control event.
*/
_sendEvent(event) {
const remoteControlEvent = Object.assign(
{ name: REMOTE_CONTROL_MESSAGE_NAME },
event
);
this._sendMessage({ data: remoteControlEvent });
}
/**
* Sends a message to Jitsi Meet.
*
* @param {Object} message the message to be sent.
*/
_sendMessage(message) {
this._channel.send({
method: 'message',
params: message
});
}
}
module.exports = RemoteControl;