/* @flow */ import { getLogger } from 'jitsi-meet-logger'; import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; 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; declare var JitsiMeetJS: Object; const ConferenceEvents = JitsiMeetJS.events.conference; 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( 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} [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(ConferenceEvents.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(ConferenceEvents.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); } }