ref: Extract remote control and SS to jitsi-meet-electron-utils
This commit is contained in:
parent
f9f5f7692b
commit
03dd26597b
|
@ -10,10 +10,6 @@ from config.js
|
|||
```bash
|
||||
npm install
|
||||
```
|
||||
Since node_addons/sourceId2Coordinates add-on is local dependency, every code change requires increasing the version in its package.json. To rebuild the add-on if it is already installed execute:
|
||||
```bash
|
||||
npm update
|
||||
```
|
||||
|
||||
## Statring the application
|
||||
```bash
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
module.exports = {
|
||||
/**
|
||||
* Types of remote-control events.
|
||||
*/
|
||||
EVENTS: {
|
||||
mousemove: "mousemove",
|
||||
mousedown: "mousedown",
|
||||
mouseup: "mouseup",
|
||||
mousedblclick: "mousedblclick",
|
||||
mousescroll: "mousescroll",
|
||||
keydown: "keydown",
|
||||
keyup: "keyup",
|
||||
stop: "stop",
|
||||
supported: "supported"
|
||||
},
|
||||
|
||||
/**
|
||||
* Types of remote-control requests.
|
||||
*/
|
||||
REQUESTS: {
|
||||
start: "start"
|
||||
},
|
||||
|
||||
/**
|
||||
* The name of remote control messages.
|
||||
*/
|
||||
REMOTE_CONTROL_MESSAGE_NAME: "remote-control"
|
||||
};
|
|
@ -1,241 +0,0 @@
|
|||
let robot = require("robotjs");
|
||||
const sourceId2Coordinates = require("sourceId2Coordinates");
|
||||
const electron = require("electron");
|
||||
const constants = require("../../modules/remotecontrol/constants");
|
||||
const { EVENTS, REQUESTS, REMOTE_CONTROL_MESSAGE_NAME } = constants;
|
||||
|
||||
/**
|
||||
* Attaching to the window for debug purposes.
|
||||
* We should remove this in production.
|
||||
*/
|
||||
window.robot = robot;
|
||||
|
||||
/**
|
||||
* Mouse button mapping between the values in remote control mouse event and
|
||||
* robotjs methods.
|
||||
*/
|
||||
const MOUSE_BUTTONS = {
|
||||
1: "left",
|
||||
2: "middle",
|
||||
3: "right"
|
||||
};
|
||||
|
||||
/**
|
||||
* Mouse actions mapping between the values in remote control mouse event and
|
||||
* robotjs methods.
|
||||
*/
|
||||
const MOUSE_ACTIONS_FROM_EVENT_TYPE = {
|
||||
"mousedown": "down",
|
||||
"mouseup": "up"
|
||||
};
|
||||
|
||||
/**
|
||||
* Key actions mapping between the values in remote control key event and
|
||||
* robotjs methods.
|
||||
*/
|
||||
const KEY_ACTIONS_FROM_EVENT_TYPE = {
|
||||
"keydown": "down",
|
||||
"keyup": "up"
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
let mouseButtonStatus = "up";
|
||||
|
||||
/**
|
||||
* Parses the remote control events and executes them via robotjs.
|
||||
*/
|
||||
class RemoteControl {
|
||||
/**
|
||||
* Constructs new instance and initializes the remote control functionality.
|
||||
*
|
||||
* @param {Postis} channel the postis channel.
|
||||
*/
|
||||
constructor(channel) {
|
||||
// TODO: if no channel is passed, create one.
|
||||
this.channel = channel;
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the remote control functionality.
|
||||
*/
|
||||
dispose() {
|
||||
this.channel = null;
|
||||
this.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
|
||||
switch(displays.length) {
|
||||
case 0:
|
||||
this.display = undefined;
|
||||
break;
|
||||
case 1:
|
||||
// On Linux probably we'll end up here even if there are
|
||||
// multiple monitors.
|
||||
this.display = displays[0];
|
||||
break;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
default: // > 1 display
|
||||
const coordinates = sourceId2Coordinates(sourceId);
|
||||
if(coordinates) {
|
||||
// Currently sourceId2Coordinates will return undefined for
|
||||
// any OS except Windows. This code will be executed only on
|
||||
// Windows.
|
||||
const { x, y } = coordinates;
|
||||
this.display = electron.screen.getDisplayNearestPoint({
|
||||
x: x + 1,
|
||||
y: y + 1
|
||||
});
|
||||
} else {
|
||||
// On Mac OS the sourceId = 'screen' + displayId.
|
||||
// Try to match displayId with sourceId.
|
||||
const displayId = Number(sourceId.replace('screen:', ''));
|
||||
this.display
|
||||
= displays.find(display => display.id === displayId);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.workArea;
|
||||
const destX = data.x * width + x;
|
||||
const destY = data.y * height + y;
|
||||
if(mouseButtonStatus === "down") {
|
||||
robot.dragMouse(destX, destY);
|
||||
} else {
|
||||
robot.moveMouse(destX, destY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EVENTS.mousedown:
|
||||
case EVENTS.mouseup: {
|
||||
mouseButtonStatus
|
||||
= MOUSE_ACTIONS_FROM_EVENT_TYPE[data.type];
|
||||
robot.mouseToggle(
|
||||
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:{
|
||||
//FIXME: implement horizontal scrolling
|
||||
if(data.y !== 0) {
|
||||
robot.scrollMouse(
|
||||
Math.abs(data.y),
|
||||
data.y > 0 ? "down" : "up"
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EVENTS.keydown:
|
||||
case EVENTS.keyup: {
|
||||
robot.keyToggle(
|
||||
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;
|
|
@ -1,33 +0,0 @@
|
|||
const electron = require("electron");
|
||||
|
||||
/**
|
||||
* Get sources available for screensharing. The callback is invoked
|
||||
* with an array of DesktopCapturerSources.
|
||||
*
|
||||
* @param {Function} callback - The success callback.
|
||||
* @param {Function} errorCallback - The callback for errors.
|
||||
* @param {Object} options - Configuration for getting sources.
|
||||
* @param {Array} options.types - Specify the desktop source types to get,
|
||||
* with valid sources being "window" and "screen".
|
||||
* @param {Object} options.thumbnailSize - Specify how big the preview
|
||||
* images for the sources should be. The valid keys are height and width,
|
||||
* e.g. { height: number, width: number}. By default electron will return
|
||||
* images with height and width of 150px.
|
||||
*/
|
||||
function obtainDesktopStreams(callback, errorCallback, options = {}) {
|
||||
electron.desktopCapturer.getSources(options,
|
||||
(error, sources) => {
|
||||
if (error) {
|
||||
errorCallback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(sources);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function setupScreenSharingForWindow(pWindow) {
|
||||
pWindow.JitsiMeetElectron = {
|
||||
obtainDesktopStreams
|
||||
};
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
'targets': [{
|
||||
'target_name': 'sourceId2Coordinates',
|
||||
'include_dirs': [
|
||||
"<!(node -e \"require('nan')\")"
|
||||
],
|
||||
|
||||
'cflags': [
|
||||
'-Wall',
|
||||
'-Wparentheses',
|
||||
'-Winline',
|
||||
'-Wbad-function-cast',
|
||||
'-Wdisabled-optimization'
|
||||
],
|
||||
|
||||
'conditions': [
|
||||
["OS=='win'", {
|
||||
'defines': ['IS_WINDOWS']
|
||||
}]
|
||||
],
|
||||
|
||||
'sources': [
|
||||
'src/index.cc',
|
||||
'src/sourceId2Coordinates.cc'
|
||||
]
|
||||
}]
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
const sourceId2Coordinates
|
||||
= require('./build/Release/sourceId2Coordinates.node').sourceId2Coordinates;
|
||||
|
||||
/**
|
||||
* Returns the coordinates of a desktop using the passed desktop sharing source
|
||||
* id.
|
||||
*
|
||||
* @param {string} sourceId - The desktop sharing source id.
|
||||
* @returns {Object.<string, number>|undefined} - The x and y coordinates of the
|
||||
* top left corner of the desktop. Currently works only for windows. Returns
|
||||
* undefined for Mac OS, Linux.
|
||||
*/
|
||||
module.exports = function(sourceID) {
|
||||
// On windows the source id will have the following format "0:desktop_id".
|
||||
// we need the "desktop_id" only to get the coordinates.
|
||||
const idArr = sourceID.split(":");
|
||||
const id = Number(idArr.length > 1 ? idArr[1] : sourceID);
|
||||
if(id) {
|
||||
return sourceId2Coordinates(id);
|
||||
}
|
||||
return undefined;
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"name": "sourceId2Coordinates",
|
||||
"version": "0.0.3",
|
||||
"description": "Native addon that returns the coordinates of desktop using the passed source id",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"install": "node-gyp rebuild"
|
||||
},
|
||||
"keywords": [
|
||||
"Util",
|
||||
"WebRTC"
|
||||
],
|
||||
"gypfile": true,
|
||||
"dependencies": {
|
||||
"nan": "^2.2.1"
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
#include <node.h>
|
||||
#include <nan.h>
|
||||
#include <v8.h>
|
||||
#include <stdio.h>
|
||||
#include "sourceId2Coordinates.h"
|
||||
|
||||
using namespace v8;
|
||||
|
||||
NAN_METHOD(sourceId2Coordinates)
|
||||
{
|
||||
const int sourceID = info[0]->Int32Value();
|
||||
Local<Object> obj = Nan::New<Object>();
|
||||
Point coordinates;
|
||||
if(!sourceId2Coordinates(sourceID, &coordinates))
|
||||
{ // return undefined if sourceId2Coordinates function fail.
|
||||
info.GetReturnValue().Set(Nan::Undefined());
|
||||
}
|
||||
else
|
||||
{ // return the coordinates if sourceId2Coordinates function succeed.
|
||||
Nan::Set(obj, Nan::New("x").ToLocalChecked(), Nan::New(coordinates.x));
|
||||
Nan::Set(obj, Nan::New("y").ToLocalChecked(), Nan::New(coordinates.y));
|
||||
info.GetReturnValue().Set(obj);
|
||||
}
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(Init)
|
||||
{
|
||||
Nan::Set(
|
||||
target,
|
||||
Nan::New("sourceId2Coordinates").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<FunctionTemplate>(sourceId2Coordinates))
|
||||
.ToLocalChecked()
|
||||
);
|
||||
NAN_EXPORT(target, sourceId2Coordinates);
|
||||
}
|
||||
|
||||
NODE_MODULE(sourceId2CoordinatesModule, Init)
|
|
@ -1,45 +0,0 @@
|
|||
#if defined(IS_WINDOWS)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "sourceId2Coordinates.h"
|
||||
|
||||
/**
|
||||
* Tries to get the coordinates of a desktop from passed sourceId
|
||||
* (which identifies a desktop sharing source). Used to match the source id to a
|
||||
* screen in Electron.
|
||||
*
|
||||
* Returns true on success and false on failure.
|
||||
*
|
||||
* NOTE: Works on windows only because on the other platforms there is an easier
|
||||
* way to match the source id and the screen.
|
||||
*/
|
||||
bool sourceId2Coordinates(int sourceId, Point* res)
|
||||
{
|
||||
#if defined(IS_WINDOWS)
|
||||
DISPLAY_DEVICE device;
|
||||
device.cb = sizeof(device);
|
||||
|
||||
if (!EnumDisplayDevices(NULL, sourceId, &device, 0) // device not found
|
||||
|| !(device.StateFlags & DISPLAY_DEVICE_ACTIVE))// device is not active
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DEVMODE deviceSettings;
|
||||
deviceSettings.dmSize = sizeof(deviceSettings);
|
||||
deviceSettings.dmDriverExtra = 0;
|
||||
if(!EnumDisplaySettingsEx(device.DeviceName, ENUM_CURRENT_SETTINGS,
|
||||
&deviceSettings, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
res->x = deviceSettings.dmPosition.x;
|
||||
res->y = deviceSettings.dmPosition.y;
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
struct Point {
|
||||
int x;
|
||||
int y;
|
||||
Point(): x(0), y(0) {};
|
||||
};
|
||||
|
||||
bool sourceId2Coordinates(int sourceId, Point* res);
|
|
@ -30,9 +30,7 @@
|
|||
"readmeFilename": "README.md",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"sourceId2Coordinates": "file:./node_addons/sourceId2Coordinates",
|
||||
"robotjs": "jitsi/robotjs#jitsi",
|
||||
"postis": "^2.2.0"
|
||||
"jitsi-meet-electron-utils": "jitsi/jitsi-meet-electron-utils"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "1.4.13",
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
/* global process */
|
||||
const RemoteControl = require("../../modules/remotecontrol");
|
||||
let postis = require("postis");
|
||||
const setupScreenSharingForWindow = require("../../modules/screensharing");
|
||||
const utils = require("jitsi-meet-electron-utils");
|
||||
const {
|
||||
RemoteControl,
|
||||
setupScreenSharingForWindow
|
||||
} = utils;
|
||||
const config = require("../../config.js");
|
||||
|
||||
/**
|
||||
* The postis channel.
|
||||
*/
|
||||
let channel;
|
||||
|
||||
/**
|
||||
* The remote control instance.
|
||||
*/
|
||||
|
@ -32,18 +29,12 @@ document.body.appendChild(iframe);
|
|||
function onload() {
|
||||
setupScreenSharingForWindow(iframe.contentWindow);
|
||||
iframe.contentWindow.onunload = onunload;
|
||||
channel = postis({
|
||||
window: iframe.contentWindow,
|
||||
windowForEventListening: window
|
||||
});
|
||||
remoteControl = new RemoteControl(channel);
|
||||
remoteControl = new RemoteControl(iframe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the postis objects and remoteControl.
|
||||
*/
|
||||
function onunload() {
|
||||
channel.destroy();
|
||||
channel = null;
|
||||
remoteControl.dispose();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue