mirror of https://github.com/jitsi/jitsi-meet
parent
f88061db06
commit
af6c794fda
@ -1,4 +0,0 @@ |
||||
/** |
||||
* Notifies interested parties that hangup procedure will start. |
||||
*/ |
||||
export const BEFORE_HANGUP = 'conference.before_hangup'; |
@ -1,3 +0,0 @@ |
||||
module.exports = { |
||||
'extends': '../../react/.eslintrc.js' |
||||
}; |
@ -1,474 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import { getLogger } from 'jitsi-meet-logger'; |
||||
|
||||
import { |
||||
JitsiConferenceEvents |
||||
} from '../../react/features/base/lib-jitsi-meet'; |
||||
import UIEvents from '../../service/UI/UIEvents'; |
||||
import { |
||||
EVENTS, |
||||
PERMISSIONS_ACTIONS, |
||||
REMOTE_CONTROL_MESSAGE_NAME |
||||
} from '../../service/remotecontrol/Constants'; |
||||
import * as RemoteControlEvents |
||||
from '../../service/remotecontrol/RemoteControlEvents'; |
||||
import * as KeyCodes from '../keycode/keycode'; |
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant'; |
||||
|
||||
declare var $: Function; |
||||
declare var APP: Object; |
||||
|
||||
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<boolean>} Resolve values - true(accept), false(deny), |
||||
* null(the participant has left). |
||||
*/ |
||||
requestPermissions( |
||||
userId: string, |
||||
eventCaptureArea: Object |
||||
): Promise<boolean | null> { |
||||
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( |
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
permissionsReplyListener); |
||||
APP.conference.removeConferenceListener( |
||||
JitsiConferenceEvents.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( |
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
permissionsReplyListener); |
||||
APP.conference.addConferenceListener( |
||||
JitsiConferenceEvents.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 {boolean|null} |
||||
*/ |
||||
_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( |
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
this._stopListener); |
||||
APP.conference.addConferenceListener(JitsiConferenceEvents.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() { |
||||
let area; |
||||
|
||||
if (!this._enabled |
||||
|| this._isCollectingEvents |
||||
|| !(area = this._area)) { |
||||
return; |
||||
} |
||||
logger.log('Resuming remote control controller.'); |
||||
this._isCollectingEvents = true; |
||||
APP.keyboardshortcut.enable(false); |
||||
|
||||
area.mousemove(event => { |
||||
const area = this._area; // eslint-disable-line no-shadow
|
||||
|
||||
if (!area) { |
||||
return; |
||||
} |
||||
|
||||
const position = area.position(); |
||||
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, { |
||||
type: EVENTS.mousemove, |
||||
x: (event.pageX - position.left) / area.width(), |
||||
y: (event.pageY - position.top) / area.height() |
||||
}); |
||||
}); |
||||
|
||||
area.mousedown(this._onMouseClickHandler.bind(this, EVENTS.mousedown)); |
||||
area.mouseup(this._onMouseClickHandler.bind(this, EVENTS.mouseup)); |
||||
|
||||
area.dblclick( |
||||
this._onMouseClickHandler.bind(this, EVENTS.mousedblclick)); |
||||
|
||||
area.contextmenu(() => false); |
||||
|
||||
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( |
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
this._stopListener); |
||||
APP.conference.removeConferenceListener(JitsiConferenceEvents.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); |
||||
|
||||
const area = this._area; |
||||
|
||||
if (area) { |
||||
area.off('contextmenu'); |
||||
area.off('dblclick'); |
||||
area.off('mousedown'); |
||||
area.off('mousemove'); |
||||
area.off('mouseup'); |
||||
|
||||
area[0].onmousewheel = undefined; |
||||
} |
||||
|
||||
$(window).off('keydown'); |
||||
$(window).off('keyup'); |
||||
} |
||||
|
||||
/** |
||||
* 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(); |
||||
} |
||||
} |
||||
} |
@ -1,331 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import { getLogger } from 'jitsi-meet-logger'; |
||||
|
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; |
||||
import { |
||||
JitsiConferenceEvents |
||||
} from '../../react/features/base/lib-jitsi-meet'; |
||||
import { |
||||
openRemoteControlAuthorizationDialog |
||||
} from '../../react/features/remote-control'; |
||||
import { |
||||
DISCO_REMOTE_CONTROL_FEATURE, |
||||
EVENTS, |
||||
PERMISSIONS_ACTIONS, |
||||
REMOTE_CONTROL_MESSAGE_NAME, |
||||
REQUESTS |
||||
} from '../../service/remotecontrol/Constants'; |
||||
import * as RemoteControlEvents |
||||
from '../../service/remotecontrol/RemoteControlEvents'; |
||||
import { Transport, PostMessageTransportBackend } from '../transport'; |
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant'; |
||||
|
||||
declare var APP: Object; |
||||
declare var config: Object; |
||||
declare var interfaceConfig: Object; |
||||
|
||||
const logger = getLogger(__filename); |
||||
|
||||
/** |
||||
* The transport instance used for communication with external apps. |
||||
* |
||||
* @type {Transport} |
||||
*/ |
||||
const transport = new Transport({ |
||||
backend: new PostMessageTransportBackend({ |
||||
postisOptions: { scope: 'jitsi-remote-control' } |
||||
}) |
||||
}); |
||||
|
||||
/** |
||||
* 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 { |
||||
_controller: ?string; |
||||
_enabled: boolean; |
||||
_hangupListener: Function; |
||||
_remoteControlEventsListener: Function; |
||||
_userLeftListener: Function; |
||||
|
||||
/** |
||||
* Creates new instance. |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
this._controller = null; |
||||
this._remoteControlEventsListener |
||||
= this._onRemoteControlMessage.bind(this); |
||||
this._userLeftListener = this._onUserLeft.bind(this); |
||||
this._hangupListener = this._onHangup.bind(this); |
||||
|
||||
// We expect here that even if we receive the supported event earlier
|
||||
// it will be cached and we'll receive it.
|
||||
transport.on('event', event => { |
||||
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) { |
||||
this._onRemoteControlAPIEvent(event); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Enables / Disables the remote control. |
||||
* |
||||
* @param {boolean} enabled - The new state. |
||||
* @returns {void} |
||||
*/ |
||||
_enable(enabled: boolean) { |
||||
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( |
||||
JitsiConferenceEvents.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( |
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, |
||||
this._remoteControlEventsListener); |
||||
APP.conference.removeListener( |
||||
JitsiMeetConferenceEvents.BEFORE_HANGUP, |
||||
this._hangupListener); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Removes the listener for JitsiConferenceEvents.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} [dontNotify] - If true - a notification about stopping |
||||
* the remote control won't be displayed. |
||||
* @returns {void} |
||||
*/ |
||||
_stop(dontNotify: boolean = false) { |
||||
if (!this._controller) { |
||||
return; |
||||
} |
||||
logger.log('Remote control receiver stop.'); |
||||
this._controller = null; |
||||
APP.conference.removeConferenceListener( |
||||
JitsiConferenceEvents.USER_LEFT, |
||||
this._userLeftListener); |
||||
transport.sendEvent({ |
||||
name: REMOTE_CONTROL_MESSAGE_NAME, |
||||
type: EVENTS.stop |
||||
}); |
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); |
||||
if (!dontNotify) { |
||||
APP.UI.messageHandler.notify( |
||||
'dialog.remoteControlTitle', |
||||
'dialog.remoteControlStopMessage' |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Calls this._stop() and sends stop message to the controller participant. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
stop() { |
||||
if (!this._controller) { |
||||
return; |
||||
} |
||||
this.sendRemoteControlEndpointMessage(this._controller, { |
||||
type: EVENTS.stop |
||||
}); |
||||
this._stop(); |
||||
} |
||||
|
||||
/** |
||||
* Listens for data channel EndpointMessage. Handles only remote control |
||||
* messages. Sends the remote control messages to the external app that |
||||
* will execute them. |
||||
* |
||||
* @param {JitsiParticipant} participant - The controller participant. |
||||
* @param {Object} message - EndpointMessage from the data channels. |
||||
* @param {string} message.name - The function processes only messages with |
||||
* name REMOTE_CONTROL_MESSAGE_NAME. |
||||
* @returns {void} |
||||
*/ |
||||
_onRemoteControlMessage(participant: Object, message: Object) { |
||||
if (message.name !== REMOTE_CONTROL_MESSAGE_NAME) { |
||||
return; |
||||
} |
||||
|
||||
if (this._enabled) { |
||||
if (this._controller === null |
||||
&& message.type === EVENTS.permissions |
||||
&& message.action === PERMISSIONS_ACTIONS.request) { |
||||
const userId = participant.getId(); |
||||
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true); |
||||
APP.store.dispatch( |
||||
openRemoteControlAuthorizationDialog(userId)); |
||||
} else if (this._controller === participant.getId()) { |
||||
if (message.type === EVENTS.stop) { |
||||
this._stop(); |
||||
} else { // forward the message
|
||||
transport.sendEvent(message); |
||||
} |
||||
} // else ignore
|
||||
} else { |
||||
logger.log('Remote control message is ignored because remote ' |
||||
+ 'control is disabled', message); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Denies remote control access for user associated with the passed user id. |
||||
* |
||||
* @param {string} userId - The id associated with the user who sent the |
||||
* request for remote control authorization. |
||||
* @returns {void} |
||||
*/ |
||||
deny(userId: string) { |
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false); |
||||
this.sendRemoteControlEndpointMessage(userId, { |
||||
type: EVENTS.permissions, |
||||
action: PERMISSIONS_ACTIONS.deny |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Grants remote control access to user associated with the passed user id. |
||||
* |
||||
* @param {string} userId - The id associated with the user who sent the |
||||
* request for remote control authorization. |
||||
* @returns {void} |
||||
*/ |
||||
grant(userId: string) { |
||||
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT, |
||||
this._userLeftListener); |
||||
this._controller = userId; |
||||
logger.log(`Remote control permissions granted to: ${userId}`); |
||||
|
||||
let promise; |
||||
|
||||
if (APP.conference.isSharingScreen |
||||
&& APP.conference.getDesktopSharingSourceType() === 'screen') { |
||||
promise = this._sendStartRequest(); |
||||
} else { |
||||
promise = APP.conference.toggleScreenSharing( |
||||
true, |
||||
{ |
||||
desktopSharingSources: [ 'screen' ] |
||||
}) |
||||
.then(() => this._sendStartRequest()); |
||||
} |
||||
|
||||
promise |
||||
.then(() => |
||||
this.sendRemoteControlEndpointMessage(userId, { |
||||
type: EVENTS.permissions, |
||||
action: PERMISSIONS_ACTIONS.grant |
||||
}) |
||||
) |
||||
.catch(error => { |
||||
logger.error(error); |
||||
|
||||
this.sendRemoteControlEndpointMessage(userId, { |
||||
type: EVENTS.permissions, |
||||
action: PERMISSIONS_ACTIONS.error |
||||
}); |
||||
|
||||
APP.UI.messageHandler.notify( |
||||
'dialog.remoteControlTitle', |
||||
'dialog.startRemoteControlErrorMessage' |
||||
); |
||||
|
||||
this._stop(true); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Sends remote control start request. |
||||
* |
||||
* @returns {Promise} |
||||
*/ |
||||
_sendStartRequest() { |
||||
return transport.sendRequest({ |
||||
name: REMOTE_CONTROL_MESSAGE_NAME, |
||||
type: REQUESTS.start, |
||||
sourceId: APP.conference.getDesktopSharingSourceId() |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Handles remote control events from the external app. Currently only |
||||
* events with type EVENTS.supported and EVENTS.stop are |
||||
* supported. |
||||
* |
||||
* @param {RemoteControlEvent} event - The remote control event. |
||||
* @returns {void} |
||||
*/ |
||||
_onRemoteControlAPIEvent(event: Object) { |
||||
switch (event.type) { |
||||
case EVENTS.supported: |
||||
this._onRemoteControlSupported(); |
||||
break; |
||||
case EVENTS.stop: |
||||
this.stop(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles events for support for executing remote control events into |
||||
* the wrapper application. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onRemoteControlSupported() { |
||||
logger.log('Remote Control supported.'); |
||||
if (config.disableRemoteControl) { |
||||
logger.log('Remote Control disabled.'); |
||||
} else { |
||||
this._enable(true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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._controller === id) { |
||||
this._stop(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles hangup events. Disables the receiver. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onHangup() { |
||||
this._enable(false); |
||||
} |
||||
} |
@ -1,98 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import EventEmitter from 'events'; |
||||
import { getLogger } from 'jitsi-meet-logger'; |
||||
|
||||
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet'; |
||||
import { DISCO_REMOTE_CONTROL_FEATURE } |
||||
from '../../service/remotecontrol/Constants'; |
||||
import * as RemoteControlEvents |
||||
from '../../service/remotecontrol/RemoteControlEvents'; |
||||
|
||||
import Controller from './Controller'; |
||||
import Receiver from './Receiver'; |
||||
|
||||
const logger = getLogger(__filename); |
||||
|
||||
declare var APP: Object; |
||||
declare var config: Object; |
||||
|
||||
/** |
||||
* Implements the remote control functionality. |
||||
*/ |
||||
class RemoteControl extends EventEmitter { |
||||
_active: boolean; |
||||
_initialized: boolean; |
||||
controller: Controller; |
||||
receiver: Receiver; |
||||
|
||||
/** |
||||
* Constructs new instance. Creates controller and receiver properties. |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
this.controller = new Controller(); |
||||
this._active = false; |
||||
this._initialized = false; |
||||
|
||||
this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => { |
||||
this.active = active; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Sets the remote control session active status. |
||||
* |
||||
* @param {boolean} isActive - True - if the controller or the receiver is |
||||
* currently in remote control session and false otherwise. |
||||
* @returns {void} |
||||
*/ |
||||
set active(isActive: boolean) { |
||||
this._active = isActive; |
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive); |
||||
} |
||||
|
||||
/** |
||||
* Returns the remote control session active status. |
||||
* |
||||
* @returns {boolean} - True - if the controller or the receiver is |
||||
* currently in remote control session and false otherwise. |
||||
*/ |
||||
get active(): boolean { |
||||
return this._active; |
||||
} |
||||
|
||||
/** |
||||
* Initializes the remote control - checks if the remote control should be |
||||
* enabled or not. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
init() { |
||||
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) { |
||||
return; |
||||
} |
||||
logger.log('Initializing remote control.'); |
||||
this._initialized = true; |
||||
this.controller.enable(true); |
||||
this.receiver = new Receiver(); |
||||
|
||||
this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => { |
||||
this.active = active; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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: Object) { |
||||
return user.getFeatures() |
||||
.then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE)); |
||||
} |
||||
} |
||||
|
||||
export default new RemoteControl(); |
@ -1,72 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import EventEmitter from 'events'; |
||||
import { getLogger } from 'jitsi-meet-logger'; |
||||
|
||||
import { |
||||
REMOTE_CONTROL_MESSAGE_NAME |
||||
} from '../../service/remotecontrol/Constants'; |
||||
|
||||
const logger = getLogger(__filename); |
||||
|
||||
declare var APP: Object; |
||||
|
||||
/** |
||||
* Implements common logic for Receiver class and Controller class. |
||||
*/ |
||||
export default class RemoteControlParticipant extends EventEmitter { |
||||
_enabled: boolean; |
||||
|
||||
/** |
||||
* Creates new instance. |
||||
*/ |
||||
constructor() { |
||||
super(); |
||||
this._enabled = false; |
||||
} |
||||
|
||||
/** |
||||
* Enables / Disables the remote control. |
||||
* |
||||
* @param {boolean} enabled - The new state. |
||||
* @returns {void} |
||||
*/ |
||||
enable(enabled: boolean) { |
||||
this._enabled = enabled; |
||||
} |
||||
|
||||
/** |
||||
* Sends remote control message to other participant trough data channel. |
||||
* |
||||
* @param {string} to - The participant who will receive the event. |
||||
* @param {RemoteControlEvent} event - The remote control event. |
||||
* @param {Function} onDataChannelFail - Handler for data channel failure. |
||||
* @returns {void} |
||||
*/ |
||||
sendRemoteControlEndpointMessage( |
||||
to: ?string, |
||||
event: Object, |
||||
onDataChannelFail: ?Function) { |
||||
if (!this._enabled || !to) { |
||||
logger.warn( |
||||
'Remote control: Skip sending remote control event. Params:', |
||||
this.enable, |
||||
to); |
||||
|
||||
return; |
||||
} |
||||
try { |
||||
APP.conference.sendEndpointMessage(to, { |
||||
name: REMOTE_CONTROL_MESSAGE_NAME, |
||||
...event |
||||
}); |
||||
} catch (e) { |
||||
logger.error( |
||||
'Failed to send EndpointMessage via the datachannels', |
||||
e); |
||||
if (typeof onDataChannelFail === 'function') { |
||||
onDataChannelFail(e); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions'; |
||||
|
||||
export default getLogger('features/base/participants'); |
@ -0,0 +1,70 @@ |
||||
// @flow
|
||||
|
||||
/** |
||||
* The type of (redux) action which signals that the controller is capturing mouse and keyboard events. |
||||
* |
||||
* { |
||||
* type: CAPTURE_EVENTS, |
||||
* isCapturingEvents: boolean |
||||
* } |
||||
*/ |
||||
export const CAPTURE_EVENTS = 'CAPTURE_EVENTS'; |
||||
|
||||
/** |
||||
* The type of (redux) action which signals that a remote control active state has changed. |
||||
* |
||||
* { |
||||
* type: REMOTE_CONTROL_ACTIVE, |
||||
* active: boolean |
||||
* } |
||||
*/ |
||||
export const REMOTE_CONTROL_ACTIVE = 'REMOTE_CONTROL_ACTIVE'; |
||||
|
||||
/** |
||||
* The type of (redux) action which sets the receiver transport object. |
||||
* |
||||
* { |
||||
* type: SET_RECEIVER_TRANSPORT, |
||||
* transport: Transport |
||||
* } |
||||
*/ |
||||
export const SET_RECEIVER_TRANSPORT = 'SET_RECEIVER_TRANSPORT'; |
||||
|
||||
/** |
||||
* The type of (redux) action which enables the receiver. |
||||
* |
||||
* { |
||||
* type: SET_RECEIVER_ENABLED, |
||||
* enabled: boolean |
||||
* } |
||||
*/ |
||||
export const SET_RECEIVER_ENABLED = 'SET_RECEIVER_ENABLED'; |
||||
|
||||
/** |
||||
* The type of (redux) action which sets the controller participant on the receiver side. |
||||
* { |
||||
* type: SET_CONTROLLER, |
||||
* controller: string |
||||
* } |
||||
*/ |
||||
export const SET_CONTROLLER = 'SET_CONTROLLER'; |
||||
|
||||
/** |
||||
* The type of (redux) action which sets the controlled participant on the controller side. |
||||
* { |
||||
* type: SET_CONTROLLED_PARTICIPANT, |
||||
* controlled: string |
||||
* } |
||||
*/ |
||||
export const SET_CONTROLLED_PARTICIPANT = 'SET_CONTROLLED_PARTICIPANT'; |
||||
|
||||
|
||||
/** |
||||
* The type of (redux) action which sets the requested participant on the controller side. |
||||
* { |
||||
* type: SET_REQUESTED_PARTICIPANT, |
||||
* requestedParticipant: string |
||||
* } |
||||
*/ |
||||
export const SET_REQUESTED_PARTICIPANT = 'SET_REQUESTED_PARTICIPANT'; |
||||
|
@ -0,0 +1,128 @@ |
||||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; |
||||
import JitsiMeetJS from '../base/lib-jitsi-meet'; |
||||
|
||||
import { enableReceiver, stopReceiver } from './actions'; |
||||
import { REMOTE_CONTROL_MESSAGE_NAME, EVENTS } from './constants'; |
||||
import { keyboardEventToKey } from './keycodes'; |
||||
import logger from './logger'; |
||||
|
||||
/** |
||||
* Checks if the remote contrrol is enabled. |
||||
* |
||||
* @param {*} state - The redux state. |
||||
* @returns {boolean} - True if the remote control is enabled and false otherwise. |
||||
*/ |
||||
export function isRemoteControlEnabled(state: Object) { |
||||
return !state['features/base/config'].disableRemoteControl && JitsiMeetJS.isDesktopSharingEnabled(); |
||||
} |
||||
|
||||
/** |
||||
* Sends remote control message to other participant trough data channel. |
||||
* |
||||
* @param {JitsiConference} conference - The JitsiConference object. |
||||
* @param {string} to - The participant who will receive the event. |
||||
* @param {RemoteControlEvent} event - The remote control event. |
||||
* @returns {boolean} - True if the message was sent successfully and false otherwise. |
||||
*/ |
||||
export function sendRemoteControlEndpointMessage( |
||||
conference: Object, |
||||
to: ?string, |
||||
event: Object) { |
||||
if (!to) { |
||||
logger.warn('Remote control: Skip sending remote control event. Params:', to); |
||||
|
||||
return false; |
||||
} |
||||
|
||||
try { |
||||
conference.sendEndpointMessage(to, { |
||||
name: REMOTE_CONTROL_MESSAGE_NAME, |
||||
...event |
||||
}); |
||||
|
||||
return true; |
||||
} catch (error) { |
||||
logger.error('Failed to send EndpointMessage via the datachannels', error); |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles remote control events from the external app. Currently only |
||||
* events with type EVENTS.supported and EVENTS.stop are |
||||
* supported. |
||||
* |
||||
* @param {RemoteControlEvent} event - The remote control event. |
||||
* @param {Store} store - The redux store. |
||||
* @returns {void} |
||||
*/ |
||||
export function onRemoteControlAPIEvent(event: Object, { getState, dispatch }: Object) { |
||||
switch (event.type) { |
||||
case EVENTS.supported: |
||||
logger.log('Remote Control supported.'); |
||||
if (isRemoteControlEnabled(getState())) { |
||||
dispatch(enableReceiver()); |
||||
} else { |
||||
logger.log('Remote Control disabled.'); |
||||
} |
||||
break; |
||||
case EVENTS.stop: { |
||||
dispatch(stopReceiver()); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the area used for capturing mouse and key events. |
||||
* |
||||
* @returns {JQuery} - A JQuery selector. |
||||
*/ |
||||
export function getRemoteConrolEventCaptureArea() { |
||||
return VideoLayout.getLargeVideoWrapper(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Extract the keyboard key from the keyboard event. |
||||
* |
||||
* @param {KeyboardEvent} event - The event. |
||||
* @returns {KEYS} The key that is pressed or undefined. |
||||
*/ |
||||
export function getKey(event: Object) { |
||||
return keyboardEventToKey(event); |
||||
} |
||||
|
||||
/** |
||||
* Extract the modifiers from the keyboard event. |
||||
* |
||||
* @param {KeyboardEvent} event - The event. |
||||
* @returns {Array} With possible values: "shift", "control", "alt", "command". |
||||
*/ |
||||
export function getModifiers(event: Object) { |
||||
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; |
||||
} |
||||
|
@ -0,0 +1,5 @@ |
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions'; |
||||
|
||||
export default getLogger('features/remote-control'); |
@ -0,0 +1,92 @@ |
||||
// @flow
|
||||
import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport'; |
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; |
||||
import { CONFERENCE_JOINED } from '../base/conference'; |
||||
import { PARTICIPANT_LEFT } from '../base/participants'; |
||||
import { MiddlewareRegistry } from '../base/redux'; |
||||
|
||||
import { |
||||
clearRequest, setReceiverTransport, setRemoteControlActive, stopController, stopReceiver |
||||
} from './actions'; |
||||
import { REMOTE_CONTROL_MESSAGE_NAME } from './constants'; |
||||
import { onRemoteControlAPIEvent } from './functions'; |
||||
import './subscriber'; |
||||
|
||||
/** |
||||
* The redux middleware for the remote control feature. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register(store => next => async action => { |
||||
switch (action.type) { |
||||
case APP_WILL_MOUNT: { |
||||
const { dispatch } = store; |
||||
|
||||
dispatch(setReceiverTransport(new Transport({ |
||||
backend: new PostMessageTransportBackend({ |
||||
postisOptions: { scope: 'jitsi-remote-control' } |
||||
}) |
||||
}))); |
||||
|
||||
break; |
||||
} |
||||
case APP_WILL_UNMOUNT: { |
||||
const { getState, dispatch } = store; |
||||
const { transport } = getState()['features/remote-control'].receiver; |
||||
|
||||
if (transport) { |
||||
transport.dispose(); |
||||
dispatch(setReceiverTransport()); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
case CONFERENCE_JOINED: { |
||||
const result = next(action); |
||||
const { getState } = store; |
||||
const { transport } = getState()['features/remote-control'].receiver; |
||||
|
||||
if (transport) { |
||||
// We expect here that even if we receive the supported event earlier
|
||||
// it will be cached and we'll receive it.
|
||||
transport.on('event', event => { |
||||
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) { |
||||
onRemoteControlAPIEvent(event, store); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
}); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
case PARTICIPANT_LEFT: { |
||||
const { getState, dispatch } = store; |
||||
const state = getState(); |
||||
const { id } = action.participant; |
||||
const { receiver, controller } = state['features/remote-control']; |
||||
const { requestedParticipant, controlled } = controller; |
||||
|
||||
if (id === controlled) { |
||||
dispatch(stopController()); |
||||
} |
||||
|
||||
if (id === requestedParticipant) { |
||||
dispatch(clearRequest()); |
||||
dispatch(setRemoteControlActive(false)); |
||||
} |
||||
|
||||
if (receiver?.controller === id) { |
||||
dispatch(stopReceiver(false, true)); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
@ -0,0 +1,68 @@ |
||||
import { ReducerRegistry, set } from '../base/redux'; |
||||
|
||||
import { |
||||
CAPTURE_EVENTS, |
||||
REMOTE_CONTROL_ACTIVE, |
||||
SET_CONTROLLED_PARTICIPANT, |
||||
SET_CONTROLLER, |
||||
SET_RECEIVER_ENABLED, |
||||
SET_RECEIVER_TRANSPORT, |
||||
SET_REQUESTED_PARTICIPANT |
||||
} from './actionTypes'; |
||||
|
||||
/** |
||||
* The default state. |
||||
*/ |
||||
const DEFAULT_STATE = { |
||||
active: false, |
||||
controller: { |
||||
isCapturingEvents: false |
||||
}, |
||||
receiver: { |
||||
enabled: false |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Listen for actions that mutate the remote control state. |
||||
*/ |
||||
ReducerRegistry.register( |
||||
'features/remote-control', (state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case CAPTURE_EVENTS: |
||||
return { |
||||
...state, |
||||
controller: set(state.controller, 'isCapturingEvents', action.isCapturingEvents) |
||||
}; |
||||
case REMOTE_CONTROL_ACTIVE: |
||||
return set(state, 'active', action.active); |
||||
case SET_RECEIVER_TRANSPORT: |
||||
return { |
||||
...state, |
||||
receiver: set(state.receiver, 'transport', action.transport) |
||||
}; |
||||
case SET_RECEIVER_ENABLED: |
||||
return { |
||||
...state, |
||||
receiver: set(state.receiver, 'enabled', action.enabled) |
||||
}; |
||||
case SET_REQUESTED_PARTICIPANT: |
||||
return { |
||||
...state, |
||||
controller: set(state.controller, 'requestedParticipant', action.requestedParticipant) |
||||
}; |
||||
case SET_CONTROLLED_PARTICIPANT: |
||||
return { |
||||
...state, |
||||
controller: set(state.controller, 'controlled', action.controlled) |
||||
}; |
||||
case SET_CONTROLLER: |
||||
return { |
||||
...state, |
||||
receiver: set(state.receiver, 'controller', action.controller) |
||||
}; |
||||
} |
||||
|
||||
return state; |
||||
}, |
||||
); |
@ -0,0 +1,33 @@ |
||||
// @flow
|
||||
|
||||
import { StateListenerRegistry } from '../base/redux'; |
||||
|
||||
import { resume, pause } from './actions'; |
||||
|
||||
/** |
||||
* Listens for large video participant ID changes. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
/* selector */ state => { |
||||
const { participantId } = state['features/large-video']; |
||||
const { controller } = state['features/remote-control']; |
||||
const { controlled } = controller; |
||||
|
||||
if (!controlled) { |
||||
return undefined; |
||||
} |
||||
|
||||
return controlled === participantId; |
||||
}, |
||||
/* listener */ (isControlledParticipantOnStage, { dispatch }) => { |
||||
if (isControlledParticipantOnStage === true) { |
||||
dispatch(resume()); |
||||
} else if (isControlledParticipantOnStage === false) { |
||||
dispatch(pause()); |
||||
} |
||||
|
||||
// else {
|
||||
// isControlledParticipantOnStage === undefined. Ignore!
|
||||
// }
|
||||
} |
||||
); |
@ -1,8 +0,0 @@ |
||||
/** |
||||
* Events fired from the remote control module through the EventEmitter. |
||||
*/ |
||||
|
||||
/** |
||||
* Notifies about remote control active session status changes. |
||||
*/ |
||||
export const ACTIVE_CHANGED = 'active-changed'; |
Loading…
Reference in new issue