mirror of https://github.com/jitsi/jitsi-meet
commit
c0e80c14f8
@ -0,0 +1,4 @@ |
||||
/** |
||||
* Notifies interested parties that hangup procedure will start. |
||||
*/ |
||||
export const BEFORE_HANGUP = "conference.before_hangup"; |
||||
@ -0,0 +1,163 @@ |
||||
/** |
||||
* Enumerates the supported keys. |
||||
* NOTE: The maps represents physical keys on the keyboard, not chars. |
||||
* @readonly |
||||
* @enum {string} |
||||
*/ |
||||
export const KEYS = { |
||||
BACKSPACE: "backspace" , |
||||
DELETE : "delete", |
||||
RETURN : "enter", |
||||
TAB : "tab", |
||||
ESCAPE : "escape", |
||||
UP : "up", |
||||
DOWN : "down", |
||||
RIGHT : "right", |
||||
LEFT : "left", |
||||
HOME : "home", |
||||
END : "end", |
||||
PAGEUP : "pageup", |
||||
PAGEDOWN : "pagedown", |
||||
|
||||
F1 : "f1", |
||||
F2 : "f2", |
||||
F3 : "f3", |
||||
F4 : "f4", |
||||
F5 : "f5", |
||||
F6 : "f6", |
||||
F7 : "f7", |
||||
F8 : "f8", |
||||
F9 : "f9", |
||||
F10 : "f10", |
||||
F11 : "f11", |
||||
F12 : "f12", |
||||
META : "command", |
||||
CMD_L: "command", |
||||
CMD_R: "command", |
||||
ALT : "alt", |
||||
CONTROL : "control", |
||||
SHIFT : "shift", |
||||
CAPS_LOCK: "caps_lock", //not supported by robotjs
|
||||
SPACE : "space", |
||||
PRINTSCREEN : "printscreen", |
||||
INSERT : "insert", |
||||
|
||||
NUMPAD_0 : "numpad_0", |
||||
NUMPAD_1 : "numpad_1", |
||||
NUMPAD_2 : "numpad_2", |
||||
NUMPAD_3 : "numpad_3", |
||||
NUMPAD_4 : "numpad_4", |
||||
NUMPAD_5 : "numpad_5", |
||||
NUMPAD_6 : "numpad_6", |
||||
NUMPAD_7 : "numpad_7", |
||||
NUMPAD_8 : "numpad_8", |
||||
NUMPAD_9 : "numpad_9", |
||||
|
||||
COMMA: ",", |
||||
|
||||
PERIOD: ".", |
||||
SEMICOLON: ";", |
||||
QUOTE: "'", |
||||
BRACKET_LEFT: "[", |
||||
BRACKET_RIGHT: "]", |
||||
BACKQUOTE: "`", |
||||
BACKSLASH: "\\", |
||||
MINUS: "-", |
||||
EQUAL: "=", |
||||
SLASH: "/" |
||||
}; |
||||
|
||||
/** |
||||
* Mapping between the key codes and keys deined in KEYS. |
||||
* The mappings are based on |
||||
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Specifications
|
||||
*/ |
||||
let keyCodeToKey = { |
||||
8: KEYS.BACKSPACE, |
||||
9: KEYS.TAB, |
||||
13: KEYS.RETURN, |
||||
16: KEYS.SHIFT, |
||||
17: KEYS.CONTROL, |
||||
18: KEYS.ALT, |
||||
20: KEYS.CAPS_LOCK, |
||||
27: KEYS.ESCAPE, |
||||
32: KEYS.SPACE, |
||||
33: KEYS.PAGEUP, |
||||
34: KEYS.PAGEDOWN, |
||||
35: KEYS.END, |
||||
36: KEYS.HOME, |
||||
37: KEYS.LEFT, |
||||
38: KEYS.UP, |
||||
39: KEYS.RIGHT, |
||||
40: KEYS.DOWN, |
||||
42: KEYS.PRINTSCREEN, |
||||
44: KEYS.PRINTSCREEN, |
||||
45: KEYS.INSERT, |
||||
46: KEYS.DELETE, |
||||
59: KEYS.SEMICOLON, |
||||
61: KEYS.EQUAL, |
||||
91: KEYS.CMD_L, |
||||
92: KEYS.CMD_R, |
||||
93: KEYS.CMD_R, |
||||
96: KEYS.NUMPAD_0, |
||||
97: KEYS.NUMPAD_1, |
||||
98: KEYS.NUMPAD_2, |
||||
99: KEYS.NUMPAD_3, |
||||
100: KEYS.NUMPAD_4, |
||||
101: KEYS.NUMPAD_5, |
||||
102: KEYS.NUMPAD_6, |
||||
103: KEYS.NUMPAD_7, |
||||
104: KEYS.NUMPAD_8, |
||||
105: KEYS.NUMPAD_9, |
||||
112: KEYS.F1, |
||||
113: KEYS.F2, |
||||
114: KEYS.F3, |
||||
115: KEYS.F4, |
||||
116: KEYS.F5, |
||||
117: KEYS.F6, |
||||
118: KEYS.F7, |
||||
119: KEYS.F8, |
||||
120: KEYS.F9, |
||||
121: KEYS.F10, |
||||
122: KEYS.F11, |
||||
123: KEYS.F12, |
||||
124: KEYS.PRINTSCREEN, |
||||
173: KEYS.MINUS, |
||||
186: KEYS.SEMICOLON, |
||||
187: KEYS.EQUAL, |
||||
188: KEYS.COMMA, |
||||
189: KEYS.MINUS, |
||||
190: KEYS.PERIOD, |
||||
191: KEYS.SLASH, |
||||
192: KEYS.BACKQUOTE, |
||||
219: KEYS.BRACKET_LEFT, |
||||
220: KEYS.BACKSLASH, |
||||
221: KEYS.BRACKET_RIGHT, |
||||
222: KEYS.QUOTE, |
||||
224: KEYS.META, |
||||
229: KEYS.SEMICOLON |
||||
}; |
||||
|
||||
/** |
||||
* Generate codes for digit keys (0-9) |
||||
*/ |
||||
for(let i = 0; i < 10; i++) { |
||||
keyCodeToKey[i + 48] = `${i}`; |
||||
} |
||||
|
||||
/** |
||||
* Generate codes for letter keys (a-z) |
||||
*/ |
||||
for(let i = 0; i < 26; i++) { |
||||
let keyCode = i + 65; |
||||
keyCodeToKey[keyCode] = String.fromCharCode(keyCode).toLowerCase(); |
||||
} |
||||
|
||||
/** |
||||
* Returns key associated with the keyCode from the passed event. |
||||
* @param {KeyboardEvent} event the event |
||||
* @returns {KEYS} the key on the keyboard. |
||||
*/ |
||||
export function keyboardEventToKey(event) { |
||||
return keyCodeToKey[event.which]; |
||||
} |
||||
@ -0,0 +1,374 @@ |
||||
/* global $, JitsiMeetJS, APP */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
import * as KeyCodes from "../keycode/keycode"; |
||||
import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS} |
||||
from "../../service/remotecontrol/Constants"; |
||||
import RemoteControlParticipant from "./RemoteControlParticipant"; |
||||
import UIEvents from "../../service/UI/UIEvents"; |
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference; |
||||
|
||||
/** |
||||
* Extract the keyboard key from the keyboard event. |
||||
* @param event {KeyboardEvent} 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 event {KeyboardEvent} the event. |
||||
* @returns {Array} with possible values: "shift", "control", "alt", "command". |
||||
*/ |
||||
function getModifiers(event) { |
||||
let 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 { |
||||
/** |
||||
* 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, eventCaptureArea) { |
||||
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) => { |
||||
const clearRequest = () => { |
||||
this.requestedParticipant = null; |
||||
APP.conference.removeConferenceListener( |
||||
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
permissionsReplyListener); |
||||
APP.conference.removeConferenceListener( |
||||
ConferenceEvents.USER_LEFT, |
||||
onUserLeft); |
||||
}; |
||||
const permissionsReplyListener = (participant, event) => { |
||||
let result = null; |
||||
try { |
||||
result = this._handleReply(participant, event); |
||||
} catch (e) { |
||||
reject(e); |
||||
} |
||||
if(result !== null) { |
||||
clearRequest(); |
||||
resolve(result); |
||||
} |
||||
}; |
||||
const 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._sendRemoteControlEvent(userId, { |
||||
type: EVENT_TYPES.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. |
||||
*/ |
||||
_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.area = null; |
||||
} |
||||
switch(remoteControlEvent.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 property. The function process only events of |
||||
* type REMOTE_CONTROL_EVENT_TYPE |
||||
* @property {RemoteControlEvent} event - 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. Sets conference |
||||
* listeners. Disables keyboard events. |
||||
*/ |
||||
_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(). |
||||
*/ |
||||
resume() { |
||||
if(!this.enabled || this.isCollectingEvents) { |
||||
return; |
||||
} |
||||
logger.log("Resuming remote control controller."); |
||||
this.isCollectingEvents = true; |
||||
APP.keyboardshortcut.enable(false); |
||||
this.area.mousemove(event => { |
||||
const position = this.area.position(); |
||||
this._sendRemoteControlEvent(this.controlledParticipant, { |
||||
type: EVENT_TYPES.mousemove, |
||||
x: (event.pageX - position.left)/this.area.width(), |
||||
y: (event.pageY - position.top)/this.area.height() |
||||
}); |
||||
}); |
||||
this.area.mousedown(this._onMouseClickHandler.bind(this, |
||||
EVENT_TYPES.mousedown)); |
||||
this.area.mouseup(this._onMouseClickHandler.bind(this, |
||||
EVENT_TYPES.mouseup)); |
||||
this.area.dblclick( |
||||
this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick)); |
||||
this.area.contextmenu(() => false); |
||||
this.area[0].onmousewheel = event => { |
||||
this._sendRemoteControlEvent(this.controlledParticipant, { |
||||
type: EVENT_TYPES.mousescroll, |
||||
x: event.deltaX, |
||||
y: event.deltaY |
||||
}); |
||||
}; |
||||
$(window).keydown(this._onKeyPessHandler.bind(this, |
||||
EVENT_TYPES.keydown)); |
||||
$(window).keyup(this._onKeyPessHandler.bind(this, EVENT_TYPES.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. |
||||
*/ |
||||
_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.controlledParticipant = null; |
||||
this.pause(); |
||||
this.area = null; |
||||
APP.UI.messageHandler.openMessageDialog( |
||||
"dialog.remoteControlTitle", |
||||
"dialog.remoteControlStopMessage" |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Executes this._stop() mehtod: |
||||
* 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. |
||||
*/ |
||||
stop() { |
||||
if(!this.controlledParticipant) { |
||||
return; |
||||
} |
||||
this._sendRemoteControlEvent(this.controlledParticipant, { |
||||
type: EVENT_TYPES.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(). |
||||
*/ |
||||
pause() { |
||||
if(!this.controlledParticipant) { |
||||
return; |
||||
} |
||||
logger.log("Pausing remote control controller."); |
||||
this.isCollectingEvents = false; |
||||
APP.keyboardshortcut.enable(true); |
||||
this.area.off( "mousemove" ); |
||||
this.area.off( "mousedown" ); |
||||
this.area.off( "mouseup" ); |
||||
this.area.off( "contextmenu" ); |
||||
this.area.off( "dblclick" ); |
||||
$(window).off( "keydown"); |
||||
$(window).off( "keyup"); |
||||
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. |
||||
*/ |
||||
_onMouseClickHandler(type, event) { |
||||
this._sendRemoteControlEvent(this.controlledParticipant, { |
||||
type: 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} this.requestedParticipant. |
||||
* 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. |
||||
*/ |
||||
_onKeyPessHandler(type, event) { |
||||
this._sendRemoteControlEvent(this.controlledParticipant, { |
||||
type: 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 |
||||
*/ |
||||
_onUserLeft(id) { |
||||
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. |
||||
*/ |
||||
_onLargeVideoIdChanged(id) { |
||||
if (!this.controlledParticipant) { |
||||
return; |
||||
} |
||||
if(this.controlledParticipant == id) { |
||||
this.resume(); |
||||
} else { |
||||
this.pause(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,192 @@ |
||||
/* global APP, JitsiMeetJS, interfaceConfig */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES, |
||||
PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants"; |
||||
import RemoteControlParticipant from "./RemoteControlParticipant"; |
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; |
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference; |
||||
|
||||
/** |
||||
* This class represents the receiver party for a remote controller session. |
||||
* It handles "remote-control-event" events and sends them to the |
||||
* API module. From there the events can be received from wrapper application |
||||
* and executed. |
||||
*/ |
||||
export default class Receiver extends RemoteControlParticipant { |
||||
/** |
||||
* Creates new instance. |
||||
* @constructor |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
this.controller = null; |
||||
this._remoteControlEventsListener |
||||
= this._onRemoteControlEvent.bind(this); |
||||
this._userLeftListener = this._onUserLeft.bind(this); |
||||
this._hangupListener = this._onHangup.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Enables / Disables the remote control |
||||
* @param {boolean} enabled the new state. |
||||
*/ |
||||
enable(enabled) { |
||||
if(this.enabled === enabled) { |
||||
return; |
||||
} |
||||
this.enabled = enabled; |
||||
if(enabled === true) { |
||||
logger.log("Remote control receiver enabled."); |
||||
// Announce remote control support.
|
||||
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true); |
||||
APP.conference.addConferenceListener( |
||||
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
this._remoteControlEventsListener); |
||||
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP, |
||||
this._hangupListener); |
||||
} else { |
||||
logger.log("Remote control receiver disabled."); |
||||
this._stop(true); |
||||
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE); |
||||
APP.conference.removeConferenceListener( |
||||
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
this._remoteControlEventsListener); |
||||
APP.conference.removeListener( |
||||
JitsiMeetConferenceEvents.BEFORE_HANGUP, |
||||
this._hangupListener); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Removes the listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED |
||||
* events. Sends stop message to the wrapper application. Optionally |
||||
* displays dialog for informing the user that remote control session |
||||
* ended. |
||||
* @param {boolean} dontShowDialog - if true the dialog won't be displayed. |
||||
*/ |
||||
_stop(dontShowDialog = false) { |
||||
if(!this.controller) { |
||||
return; |
||||
} |
||||
logger.log("Remote control receiver stop."); |
||||
this.controller = null; |
||||
APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT, |
||||
this._userLeftListener); |
||||
APP.API.sendRemoteControlEvent({ |
||||
type: EVENT_TYPES.stop |
||||
}); |
||||
if(!dontShowDialog) { |
||||
APP.UI.messageHandler.openMessageDialog( |
||||
"dialog.remoteControlTitle", |
||||
"dialog.remoteControlStopMessage" |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Calls this._stop() and sends stop message to the controller participant |
||||
*/ |
||||
stop() { |
||||
if(!this.controller) { |
||||
return; |
||||
} |
||||
this._sendRemoteControlEvent(this.controller, { |
||||
type: EVENT_TYPES.stop |
||||
}); |
||||
this._stop(); |
||||
} |
||||
|
||||
/** |
||||
* Listens for data channel EndpointMessage events. Handles only events of |
||||
* type remote control. Sends "remote-control-event" events to the API |
||||
* module. |
||||
* @param {JitsiParticipant} participant the controller participant |
||||
* @param {Object} event EndpointMessage event from the data channels. |
||||
* @property {string} type property. The function process only events of |
||||
* type REMOTE_CONTROL_EVENT_TYPE |
||||
* @property {RemoteControlEvent} event - the remote control event. |
||||
*/ |
||||
_onRemoteControlEvent(participant, 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() |
||||
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; |
||||
remoteControlEvent.screenSharing |
||||
= APP.conference.isSharingScreen; |
||||
} else if(this.controller !== participant.getId()) { |
||||
return; |
||||
} else if(remoteControlEvent.type === EVENT_TYPES.stop) { |
||||
this._stop(); |
||||
return; |
||||
} |
||||
APP.API.sendRemoteControlEvent(remoteControlEvent); |
||||
} else if(event.type === REMOTE_CONTROL_EVENT_TYPE) { |
||||
logger.log("Remote control event is ignored because remote " |
||||
+ "control is disabled", event); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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) { |
||||
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, |
||||
this._userLeftListener); |
||||
this.controller = userId; |
||||
logger.log("Remote control permissions granted to: " + userId); |
||||
if(!APP.conference.isSharingScreen) { |
||||
APP.conference.toggleScreenSharing(); |
||||
APP.conference.screenSharingPromise.then(() => { |
||||
if(APP.conference.isSharingScreen) { |
||||
this._sendRemoteControlEvent(userId, { |
||||
type: EVENT_TYPES.permissions, |
||||
action: action |
||||
}); |
||||
} else { |
||||
this._sendRemoteControlEvent(userId, { |
||||
type: EVENT_TYPES.permissions, |
||||
action: PERMISSIONS_ACTIONS.error |
||||
}); |
||||
} |
||||
}).catch(() => { |
||||
this._sendRemoteControlEvent(userId, { |
||||
type: EVENT_TYPES.permissions, |
||||
action: PERMISSIONS_ACTIONS.error |
||||
}); |
||||
}); |
||||
return; |
||||
} |
||||
} |
||||
this._sendRemoteControlEvent(userId, { |
||||
type: EVENT_TYPES.permissions, |
||||
action: action |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Calls the stop method if the other side have left. |
||||
* @param {string} id - the user id for the participant that have left |
||||
*/ |
||||
_onUserLeft(id) { |
||||
if(this.controller === id) { |
||||
this._stop(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles hangup events. Disables the receiver. |
||||
*/ |
||||
_onHangup() { |
||||
this.enable(false); |
||||
} |
||||
} |
||||
@ -0,0 +1,89 @@ |
||||
/* global APP, config */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
import Controller from "./Controller"; |
||||
import Receiver from "./Receiver"; |
||||
import {EVENT_TYPES, DISCO_REMOTE_CONTROL_FEATURE} |
||||
from "../../service/remotecontrol/Constants"; |
||||
|
||||
/** |
||||
* Implements the remote control functionality. |
||||
*/ |
||||
class RemoteControl { |
||||
/** |
||||
* Constructs new instance. Creates controller and receiver properties. |
||||
* @constructor |
||||
*/ |
||||
constructor() { |
||||
this.controller = new Controller(); |
||||
this.receiver = new Receiver(); |
||||
this.enabled = false; |
||||
this.initialized = false; |
||||
} |
||||
|
||||
/** |
||||
* Initializes the remote control - checks if the remote control should be |
||||
* enabled or not, initializes the API module. |
||||
*/ |
||||
init() { |
||||
if(config.disableRemoteControl || this.initialized |
||||
|| !APP.conference.isDesktopSharingEnabled) { |
||||
return; |
||||
} |
||||
logger.log("Initializing remote control."); |
||||
this.initialized = true; |
||||
APP.API.init({ |
||||
forceEnable: true, |
||||
}); |
||||
this.controller.enable(true); |
||||
if(this.enabled) { // supported message came before init.
|
||||
this._onRemoteControlSupported(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles remote control events from the API module. Currently only events |
||||
* with type = EVENT_TYPES.supported or EVENT_TYPES.permissions |
||||
* @param {RemoteControlEvent} 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. |
||||
*/ |
||||
_onRemoteControlSupported() { |
||||
logger.log("Remote Control supported."); |
||||
if(!config.disableRemoteControl) { |
||||
this.enabled = true; |
||||
if(this.initialized) { |
||||
this.receiver.enable(true); |
||||
} |
||||
} else { |
||||
logger.log("Remote Control disabled."); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checks whether the passed user supports remote control or not |
||||
* @param {JitsiParticipant} user the user to be tested |
||||
* @returns {Promise<boolean>} the promise will be resolved with true if |
||||
* the user supports remote control and with false if not. |
||||
*/ |
||||
checkUserRemoteControlSupport(user) { |
||||
return user.getFeatures().then(features => |
||||
features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default new RemoteControl(); |
||||
@ -0,0 +1,42 @@ |
||||
/* global APP */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
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 {RemoteControlEvent} event the remote control event. |
||||
* @param {Function} onDataChannelFail handler for data channel failure. |
||||
*/ |
||||
_sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) { |
||||
if(!this.enabled || !to) { |
||||
logger.warn("Remote control: Skip sending remote control event." |
||||
+ " Params:", this.enable, to); |
||||
return; |
||||
} |
||||
try{ |
||||
APP.conference.sendEndpointMessage(to, |
||||
{type: REMOTE_CONTROL_EVENT_TYPE, event}); |
||||
} catch (e) { |
||||
logger.error("Failed to send EndpointMessage via the datachannels", |
||||
e); |
||||
onDataChannelFail(e); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,69 @@ |
||||
/** |
||||
* The value for the "var" attribute of feature tag in disco-info packets. |
||||
*/ |
||||
export const DISCO_REMOTE_CONTROL_FEATURE |
||||
= "http://jitsi.org/meet/remotecontrol"; |
||||
|
||||
/** |
||||
* Types of remote-control-event events. |
||||
* @readonly |
||||
* @enum {string} |
||||
*/ |
||||
export const EVENT_TYPES = { |
||||
mousemove: "mousemove", |
||||
mousedown: "mousedown", |
||||
mouseup: "mouseup", |
||||
mousedblclick: "mousedblclick", |
||||
mousescroll: "mousescroll", |
||||
keydown: "keydown", |
||||
keyup: "keyup", |
||||
permissions: "permissions", |
||||
stop: "stop", |
||||
supported: "supported" |
||||
}; |
||||
|
||||
/** |
||||
* Actions for the remote control permission events. |
||||
* @readonly |
||||
* @enum {string} |
||||
*/ |
||||
export const PERMISSIONS_ACTIONS = { |
||||
request: "request", |
||||
grant: "grant", |
||||
deny: "deny", |
||||
error: "error" |
||||
}; |
||||
|
||||
/** |
||||
* The type of remote control events sent trough the API module. |
||||
*/ |
||||
export const REMOTE_CONTROL_EVENT_TYPE = "remote-control-event"; |
||||
|
||||
/** |
||||
* The remote control event. |
||||
* @typedef {object} RemoteControlEvent |
||||
* @property {EVENT_TYPES} type - the type of the event |
||||
* @property {int} x - avaibale for type === mousemove only. The new x |
||||
* coordinate of the mouse |
||||
* @property {int} y - For mousemove type - the new y |
||||
* coordinate of the mouse and for mousescroll - represents the vertical |
||||
* scrolling diff value |
||||
* @property {int} button - 1(left), 2(middle) or 3 (right). Supported by |
||||
* mousedown, mouseup and mousedblclick types. |
||||
* @property {KEYS} key - Represents the key related to the event. Supported by |
||||
* keydown and keyup types. |
||||
* @property {KEYS[]} modifiers - Represents the modifier related to the event. |
||||
* Supported by keydown and keyup types. |
||||
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions. |
||||
* Represents the action related to the permissions event. |
||||
* |
||||
* Optional properties. Supported for permissions event for action === request: |
||||
* @property {string} userId - The user id of the participant that has sent the |
||||
* request. |
||||
* @property {string} userJID - The full JID in the MUC of the user that has |
||||
* sent the request. |
||||
* @property {string} displayName - the displayName of the participant that has |
||||
* sent the request. |
||||
* @property {boolean} screenSharing - true if the SS is started for the local |
||||
* participant and false if not. |
||||
*/ |
||||
Loading…
Reference in new issue