diff --git a/.jshintignore b/.jshintignore index 6240b0092..053199aee 100644 --- a/.jshintignore +++ b/.jshintignore @@ -8,6 +8,9 @@ node_modules/ # The following are checked by ESLint with the maximum configuration which # supersedes JSHint. flow-typed/ +modules/API/ +modules/remotecontrol/RemoteControlParticipant.js +modules/transport/ react/ # The following are checked by ESLint with the minimum configuration which does diff --git a/app.js b/app.js index 0f65aaf25..0d82d480c 100644 --- a/app.js +++ b/app.js @@ -18,7 +18,7 @@ window.toastr = require("toastr"); import UI from "./modules/UI/UI"; import settings from "./modules/settings/Settings"; import conference from './conference'; -import API from './modules/API/API'; +import API from './modules/API'; import translation from "./modules/translation/translation"; import remoteControl from "./modules/remotecontrol/RemoteControl"; diff --git a/conference.js b/conference.js index 53d6fd7bb..eddcfb32d 100644 --- a/conference.js +++ b/conference.js @@ -603,8 +603,8 @@ export default { APP.store.dispatch(showDesktopSharingButton()); - APP.remoteControl.init(); this._createRoom(tracks); + APP.remoteControl.init(); if (UIUtil.isButtonEnabled('contacts') && !interfaceConfig.filmStripOnly) { diff --git a/modules/API/API.js b/modules/API/API.js index 436435734..a429e435c 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -1,8 +1,9 @@ -/* global APP, getConfigParamsFromUrl */ - -import postisInit from 'postis'; - import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; +import { getJitsiMeetTransport } from '../transport'; + +import { API_ID } from './constants'; + +declare var APP: Object; /** * List of the available commands. @@ -18,21 +19,11 @@ let commands = {}; let initialScreenSharingState = false; /** - * JitsiMeetExternalAPI id - unique for a webpage. + * The transport instance used for communication with external apps. + * + * @type {Transport} */ -const jitsiMeetExternalApiId - = getConfigParamsFromUrl().jitsi_meet_external_api_id; - -/** - * Postis instance. Used to communicate with the external application. If - * undefined, then API is disabled. - */ -let postis; - -/** - * Object that will execute sendMessage. - */ -const target = window.opener || window.parent; +const transport = getJitsiMeetTransport(); /** * Initializes supported commands. @@ -51,12 +42,17 @@ function initCommands() { 'toggle-share-screen': toggleScreenSharing, 'video-hangup': () => APP.conference.hangup(), 'email': APP.conference.changeLocalEmail, - 'avatar-url': APP.conference.changeLocalAvatarUrl, - 'remote-control-event': - event => APP.remoteControl.onRemoteControlAPIEvent(event) + 'avatar-url': APP.conference.changeLocalAvatarUrl }; - Object.keys(commands).forEach( - key => postis.listen(key, args => commands[key](...args))); + transport.on('event', ({ data, name }) => { + if (name && commands[name]) { + commands[name](...data); + + return true; + } + + return false; + }); } /** @@ -72,25 +68,13 @@ function onDesktopSharingEnabledChanged(enabled = false) { } } -/** - * Sends message to the external application. - * - * @param {Object} message - The message to be sent. - * @returns {void} - */ -function sendMessage(message) { - if (postis) { - postis.send(message); - } -} - /** * Check whether the API should be enabled or not. * * @returns {boolean} */ function shouldBeEnabled() { - return typeof jitsiMeetExternalApiId === 'number'; + return typeof API_ID === 'number'; } /** @@ -106,21 +90,6 @@ function toggleScreenSharing() { } } -/** - * Sends event object to the external application that has been subscribed for - * that event. - * - * @param {string} name - The name event. - * @param {Object} object - Data associated with the event. - * @returns {void} - */ -function triggerEvent(name, object) { - sendMessage({ - method: name, - params: object - }); -} - /** * Implements API class that communicates with external API class and provides * interface to access Jitsi Meet features by external applications that embed @@ -137,47 +106,49 @@ class API { * module. * @returns {void} */ - init(options = {}) { - if (!shouldBeEnabled() && !options.forceEnable) { + init({ forceEnable } = {}) { + if (!shouldBeEnabled() && !forceEnable) { return; } - if (!postis) { - APP.conference.addListener( - JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, - onDesktopSharingEnabledChanged); - this._initPostis(); - } + /** + * Current status (enabled/disabled) of API. + * + * @private + * @type {boolean} + */ + this._enabled = true; + + APP.conference.addListener( + JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, + onDesktopSharingEnabledChanged); + + initCommands(); } /** - * Initializes postis library. + * Sends event to the external application. * + * @param {Object} event - The event to be sent. * @returns {void} - * - * @private */ - _initPostis() { - const postisOptions = { - window: target - }; - - if (typeof jitsiMeetExternalApiId === 'number') { - postisOptions.scope - = `jitsi_meet_external_api_${jitsiMeetExternalApiId}`; + _sendEvent(event = {}) { + if (this._enabled) { + transport.sendEvent(event); } - postis = postisInit(postisOptions); - initCommands(); } /** * Notify external application (if API is enabled) that message was sent. * - * @param {string} body - Message body. + * @param {string} message - Message body. * @returns {void} */ - notifySendingChatMessage(body) { - triggerEvent('outgoing-message', { 'message': body }); + notifySendingChatMessage(message) { + this._sendEvent({ + name: 'outgoing-message', + message + }); } /** @@ -187,21 +158,18 @@ class API { * @param {Object} options - Object with the message properties. * @returns {void} */ - notifyReceivedChatMessage(options = {}) { - const { id, nick, body, ts } = options; - + notifyReceivedChatMessage({ body, id, nick, ts } = {}) { if (APP.conference.isLocalId(id)) { return; } - triggerEvent( - 'incoming-message', - { - 'from': id, - 'message': body, - 'nick': nick, - 'stamp': ts - }); + this._sendEvent({ + name: 'incoming-message', + from: id, + message: body, + nick, + stamp: ts + }); } /** @@ -212,7 +180,10 @@ class API { * @returns {void} */ notifyUserJoined(id) { - triggerEvent('participant-joined', { id }); + this._sendEvent({ + name: 'participant-joined', + id + }); } /** @@ -223,7 +194,10 @@ class API { * @returns {void} */ notifyUserLeft(id) { - triggerEvent('participant-left', { id }); + this._sendEvent({ + name: 'participant-left', + id + }); } /** @@ -231,39 +205,43 @@ class API { * nickname. * * @param {string} id - User id. - * @param {string} displayName - User nickname. + * @param {string} displayname - User nickname. * @returns {void} */ - notifyDisplayNameChanged(id, displayName) { - triggerEvent( - 'display-name-change', - { - displayname: displayName, - id - }); + notifyDisplayNameChanged(id, displayname) { + this._sendEvent({ + name: 'display-name-change', + displayname, + id + }); } /** * Notify external application (if API is enabled) that the conference has * been joined. * - * @param {string} room - The room name. + * @param {string} roomName - The room name. * @returns {void} */ - notifyConferenceJoined(room) { - triggerEvent('video-conference-joined', { roomName: room }); + notifyConferenceJoined(roomName) { + this._sendEvent({ + name: 'video-conference-joined', + roomName + }); } /** * Notify external application (if API is enabled) that user changed their * nickname. * - * @param {string} room - User id. - * @param {string} displayName - User nickname. + * @param {string} roomName - User id. * @returns {void} */ - notifyConferenceLeft(room) { - triggerEvent('video-conference-left', { roomName: room }); + notifyConferenceLeft(roomName) { + this._sendEvent({ + name: 'video-conference-left', + roomName + }); } /** @@ -273,32 +251,17 @@ class API { * @returns {void} */ notifyReadyToClose() { - triggerEvent('video-ready-to-close', {}); + this._sendEvent({ name: 'video-ready-to-close' }); } /** - * Sends remote control event. - * - * @param {RemoteControlEvent} event - The remote control event. - * @returns {void} - */ - sendRemoteControlEvent(event) { - sendMessage({ - method: 'remote-control-event', - params: event - }); - } - - /** - * Removes the listeners. + * Disposes the allocated resources. * * @returns {void} */ dispose() { - if (postis) { - postis.destroy(); - postis = undefined; - + if (this._enabled) { + this._enabled = false; APP.conference.removeListener( JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, onDesktopSharingEnabledChanged); diff --git a/modules/API/constants.js b/modules/API/constants.js new file mode 100644 index 000000000..6f2c592da --- /dev/null +++ b/modules/API/constants.js @@ -0,0 +1,9 @@ +declare var getConfigParamsFromUrl: Function; + +/** + * JitsiMeetExternalAPI id - unique for a webpage. + */ +export const API_ID + = typeof getConfigParamsFromUrl === 'function' + ? getConfigParamsFromUrl().jitsi_meet_external_api_id + : undefined; diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 6ea00f672..a2e40867b 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -1,5 +1,9 @@ import EventEmitter from 'events'; -import postisInit from 'postis'; + +import { + PostMessageTransportBackend, + Transport +} from '../../transport'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -25,14 +29,14 @@ const commands = { * events expected by jitsi-meet */ const events = { - displayNameChange: 'display-name-change', - incomingMessage: 'incoming-message', - outgoingMessage: 'outgoing-message', - participantJoined: 'participant-joined', - participantLeft: 'participant-left', - readyToClose: 'video-ready-to-close', - videoConferenceJoined: 'video-conference-joined', - videoConferenceLeft: 'video-conference-left' + 'display-name-change': 'displayNameChange', + 'incoming-message': 'incomingMessage', + 'outgoing-message': 'outgoingMessage', + 'participant-joined': 'participantJoined', + 'participant-left': 'participantLeft', + 'video-ready-to-close': 'readyToClose', + 'video-conference-joined': 'videoConferenceJoined', + 'video-conference-left': 'videoConferenceLeft' }; /** @@ -78,8 +82,8 @@ function configToURLParamsArray(config = {}) { for (const key in config) { // eslint-disable-line guard-for-in try { - params.push(`${key}=${ - encodeURIComponent(JSON.stringify(config[key]))}`); + params.push( + `${key}=${encodeURIComponent(JSON.stringify(config[key]))}`); } catch (e) { console.warn(`Error encoding ${key}: ${e}`); } @@ -180,9 +184,13 @@ class JitsiMeetExternalAPI extends EventEmitter { }); this._createIFrame(Math.max(height, MIN_HEIGHT), Math.max(width, MIN_WIDTH)); - this.postis = postisInit({ - scope: `jitsi_meet_external_api_${id}`, - window: this.frame.contentWindow + this._transport = new Transport({ + backend: new PostMessageTransportBackend({ + postisOptions: { + scope: `jitsi_meet_external_api_${id}`, + window: this.frame.contentWindow + } + }) }); this.numberOfParticipants = 1; this._setupListeners(); @@ -225,17 +233,24 @@ class JitsiMeetExternalAPI extends EventEmitter { * @private */ _setupListeners() { - this.postis.listen('participant-joined', - changeParticipantNumber.bind(null, this, 1)); - this.postis.listen('participant-left', - changeParticipantNumber.bind(null, this, -1)); - for (const eventName in events) { // eslint-disable-line guard-for-in - const postisMethod = events[eventName]; + this._transport.on('event', ({ name, ...data }) => { + if (name === 'participant-joined') { + changeParticipantNumber(this, 1); + } else if (name === 'participant-left') { + changeParticipantNumber(this, -1); + } - this.postis.listen(postisMethod, - (...args) => this.emit(eventName, ...args)); - } + const eventName = events[name]; + + if (eventName) { + this.emit(eventName, data); + + return true; + } + + return false; + }); } /** @@ -319,10 +334,7 @@ class JitsiMeetExternalAPI extends EventEmitter { * @returns {void} */ dispose() { - if (this.postis) { - this.postis.destroy(); - this.postis = null; - } + this._transport.dispose(); this.removeAllListeners(); if (this.iframeHolder) { this.iframeHolder.parentNode.removeChild(this.iframeHolder); @@ -348,16 +360,9 @@ class JitsiMeetExternalAPI extends EventEmitter { return; } - - if (!this.postis) { - logger.error('Cannot execute command using disposed instance.'); - - return; - } - - this.postis.send({ - method: commands[name], - params: args + this._transport.sendEvent({ + data: args, + name: commands[name] }); } diff --git a/modules/API/index.js b/modules/API/index.js new file mode 100644 index 000000000..198a53aca --- /dev/null +++ b/modules/API/index.js @@ -0,0 +1,2 @@ +export default from './API'; +export * from './constants'; diff --git a/modules/remotecontrol/Controller.js b/modules/remotecontrol/Controller.js index 44f338198..51f7776fb 100644 --- a/modules/remotecontrol/Controller.js +++ b/modules/remotecontrol/Controller.js @@ -1,8 +1,11 @@ /* 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 { + EVENT_TYPES, + PERMISSIONS_ACTIONS, + REMOTE_CONTROL_EVENT_NAME +} from "../../service/remotecontrol/Constants"; import RemoteControlParticipant from "./RemoteControlParticipant"; import UIEvents from "../../service/UI/UIEvents"; @@ -132,15 +135,15 @@ export default class Controller extends RemoteControlParticipant { * @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) { + if(this.enabled + && event.name === REMOTE_CONTROL_EVENT_NAME + && event.type === EVENT_TYPES.permissions + && userId === this.requestedParticipant) { + if(event.action !== PERMISSIONS_ACTIONS.grant) { this.area = null; } - switch(remoteControlEvent.action) { + switch(event.action) { case PERMISSIONS_ACTIONS.grant: { this.controlledParticipant = userId; logger.log("Remote control permissions granted to: " @@ -166,14 +169,15 @@ export default class Controller extends RemoteControlParticipant { * @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 {string} type property. The function process only events with + * name REMOTE_CONTROL_EVENT_NAME * @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) { + if(this.enabled + && event.name === REMOTE_CONTROL_EVENT_NAME + && event.type === EVENT_TYPES.stop + && participant.getId() === this.controlledParticipant) { this._stop(); } } diff --git a/modules/remotecontrol/Receiver.js b/modules/remotecontrol/Receiver.js index cf6063617..fcf3bc25b 100644 --- a/modules/remotecontrol/Receiver.js +++ b/modules/remotecontrol/Receiver.js @@ -1,11 +1,25 @@ -/* 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"; +/* global APP, config, interfaceConfig, JitsiMeetJS */ + import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; +import { + DISCO_REMOTE_CONTROL_FEATURE, + EVENT_TYPES, + PERMISSIONS_ACTIONS, + REMOTE_CONTROL_EVENT_NAME +} from '../../service/remotecontrol/Constants'; +import { getJitsiMeetTransport } from '../transport'; + +import RemoteControlParticipant from './RemoteControlParticipant'; const ConferenceEvents = JitsiMeetJS.events.conference; +const logger = require("jitsi-meet-logger").getLogger(__filename); + +/** + * The transport instance used for communication with external apps. + * + * @type {Transport} + */ +const transport = getJitsiMeetTransport(); /** * This class represents the receiver party for a remote controller session. @@ -25,13 +39,24 @@ export default class Receiver extends RemoteControlParticipant { = this._onRemoteControlEvent.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_EVENT_NAME) { + this._onRemoteControlAPIEvent(event); + + return true; + } + + return false; + }); } /** * Enables / Disables the remote control * @param {boolean} enabled the new state. */ - enable(enabled) { + _enable(enabled) { if(this.enabled === enabled) { return; } @@ -73,7 +98,8 @@ export default class Receiver extends RemoteControlParticipant { this.controller = null; APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT, this._userLeftListener); - APP.API.sendRemoteControlEvent({ + transport.sendEvent({ + name: REMOTE_CONTROL_EVENT_NAME, type: EVENT_TYPES.stop }); if(!dontShowDialog) { @@ -103,43 +129,49 @@ export default class Receiver extends RemoteControlParticipant { * 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 {string} type property. The function process only events with + * name REMOTE_CONTROL_EVENT_NAME * @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) { + if (event.name !== REMOTE_CONTROL_EVENT_NAME) { + return; + } + + const remoteControlEvent = Object.assign({}, event); + + if (this.enabled) { + if (this.controller === null + && event.type === EVENT_TYPES.permissions + && event.action === PERMISSIONS_ACTIONS.request) { + // FIXME: Maybe use transport.sendRequest in this case??? 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()) { + } else if (this.controller !== participant.getId()) { return; - } else if(remoteControlEvent.type === EVENT_TYPES.stop) { + } else if (event.type === EVENT_TYPES.stop) { this._stop(); return; } - APP.API.sendRemoteControlEvent(remoteControlEvent); - } else if(event.type === REMOTE_CONTROL_EVENT_TYPE) { + transport.sendEvent(remoteControlEvent); + } else { logger.log("Remote control event is ignored because remote " + "control is disabled", event); } } /** - * Handles remote control permission events received from the API module. + * Handles remote control permission events. * @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) { + if (action === PERMISSIONS_ACTIONS.grant) { APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, this._userLeftListener); this.controller = userId; @@ -169,10 +201,39 @@ export default class Receiver extends RemoteControlParticipant { } this._sendRemoteControlEvent(userId, { type: EVENT_TYPES.permissions, - action: action + action }); } + /** + * Handles remote control events from the external app. 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.permissions: + this._onRemoteControlPermissionsEvent(event.userId, event.action); + break; + case EVENT_TYPES.supported: + this._onRemoteControlSupported(); + break; + } + } + + /** + * Handles events for support for executing remote control events into + * the wrapper application. + */ + _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 @@ -187,6 +248,6 @@ export default class Receiver extends RemoteControlParticipant { * Handles hangup events. Disables the receiver. */ _onHangup() { - this.enable(false); + this._enable(false); } } diff --git a/modules/remotecontrol/RemoteControl.js b/modules/remotecontrol/RemoteControl.js index 86f1f51e5..3cc629116 100644 --- a/modules/remotecontrol/RemoteControl.js +++ b/modules/remotecontrol/RemoteControl.js @@ -1,9 +1,12 @@ /* 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"; + +import { DISCO_REMOTE_CONTROL_FEATURE } + from '../../service/remotecontrol/Constants'; + +import Controller from './Controller'; +import Receiver from './Receiver'; + +const logger = require('jitsi-meet-logger').getLogger(__filename); /** * Implements the remote control functionality. @@ -15,62 +18,23 @@ class RemoteControl { */ 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. + * enabled or not. */ init() { - if(config.disableRemoteControl || this.initialized - || !APP.conference.isDesktopSharingEnabled) { + 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."); - } + this.receiver = new Receiver(); } /** @@ -80,9 +44,9 @@ class RemoteControl { * the user supports remote control and with false if not. */ checkUserRemoteControlSupport(user) { - return user.getFeatures().then(features => - features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false - ); + return user.getFeatures().then( + features => features.has(DISCO_REMOTE_CONTROL_FEATURE), + () => false); } } diff --git a/modules/remotecontrol/RemoteControlParticipant.js b/modules/remotecontrol/RemoteControlParticipant.js index 0f283ba9f..410cd4a0b 100644 --- a/modules/remotecontrol/RemoteControlParticipant.js +++ b/modules/remotecontrol/RemoteControlParticipant.js @@ -1,7 +1,10 @@ /* global APP */ + +import { + REMOTE_CONTROL_EVENT_NAME +} from "../../service/remotecontrol/Constants"; + const logger = require("jitsi-meet-logger").getLogger(__filename); -import {REMOTE_CONTROL_EVENT_TYPE} - from "../../service/remotecontrol/Constants"; export default class RemoteControlParticipant { /** @@ -26,15 +29,20 @@ export default class RemoteControlParticipant { */ _sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) { if(!this.enabled || !to) { - logger.warn("Remote control: Skip sending remote control event." - + " Params:", this.enable, 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}); + APP.conference.sendEndpointMessage(to, { + name: REMOTE_CONTROL_EVENT_NAME, + ...event + }); } catch (e) { - logger.error("Failed to send EndpointMessage via the datachannels", + logger.error( + "Failed to send EndpointMessage via the datachannels", e); onDataChannelFail(e); } diff --git a/modules/transport/.eslintrc.js b/modules/transport/.eslintrc.js new file mode 100644 index 000000000..28bcd9f77 --- /dev/null +++ b/modules/transport/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + 'extends': '../../react/.eslintrc.js' +}; diff --git a/modules/transport/PostMessageTransportBackend.js b/modules/transport/PostMessageTransportBackend.js new file mode 100644 index 000000000..d0a72d0a0 --- /dev/null +++ b/modules/transport/PostMessageTransportBackend.js @@ -0,0 +1,174 @@ +import Postis from 'postis'; + +/** + * The default options for postis. + * + * @type {Object} + */ +const DEFAULT_POSTIS_OPTIONS = { + window: window.opener || window.parent +}; + +/** + * The list of methods of incoming postis messages that we have to support for + * backward compatibility for the users that are directly sending messages to + * Jitsi Meet (without using external_api.js) + * + * @type {string[]} + */ +const LEGACY_INCOMING_METHODS = [ + 'avatar-url', + 'display-name', + 'email', + 'toggle-audio', + 'toggle-chat', + 'toggle-contact-list', + 'toggle-film-strip', + 'toggle-share-screen', + 'toggle-video', + 'video-hangup' +]; + +/** + * The list of methods of outgoing postis messages that we have to support for + * backward compatibility for the users that are directly listening to the + * postis messages send by Jitsi Meet(without using external_api.js). + * + * @type {string[]} + */ +const LEGACY_OUTGOING_METHODS = [ + 'display-name-change', + 'incoming-message', + 'outgoing-message', + 'participant-joined', + 'participant-left', + 'video-conference-joined', + 'video-conference-left', + 'video-ready-to-close' +]; + +/** + * The postis method used for all messages. + * + * @type {string} + */ +const POSTIS_METHOD_NAME = 'message'; + +/** + * Implements message transport using the postMessage API. + */ +export default class PostMessageTransportBackend { + /** + * Creates new PostMessageTransportBackend instance. + * + * @param {Object} options - Optional parameters for configuration of the + * transport. + */ + constructor({ enableLegacyFormat, postisOptions } = {}) { + this.postis = Postis({ + ...DEFAULT_POSTIS_OPTIONS, + ...postisOptions + }); + + /** + * If true PostMessageTransportBackend will process and send messages + * using the legacy format and in the same time the current format. + * Otherwise all messages (outgoing and incoming) that are using the + * legacy format will be ignored. + * + * @type {boolean} + */ + this._enableLegacyFormat = enableLegacyFormat; + + if (this._enableLegacyFormat) { + // backward compatibility + LEGACY_INCOMING_METHODS.forEach(method => + this.postis.listen( + method, + params => + this._legacyMessageReceivedCallback(method, params) + ) + ); + } + + this._receiveCallback = () => { + // Do nothing until a callback is set by the consumer of + // PostMessageTransportBackend via setReceiveCallback. + }; + + this.postis.listen( + POSTIS_METHOD_NAME, + message => this._receiveCallback(message)); + } + + /** + * Handles incoming legacy postis messages. + * + * @param {string} method - The method property from the postis message. + * @param {Any} params - The params property from the postis message. + * @returns {void} + */ + _legacyMessageReceivedCallback(method, params = {}) { + this._receiveCallback({ + data: { + name: method, + data: params + } + }); + } + + /** + * Sends the passed message via postis using the old format. + * + * @param {Object} legacyMessage - The message to be sent. + * @returns {void} + */ + _sendLegacyMessage({ name, ...data }) { + if (name && LEGACY_OUTGOING_METHODS.indexOf(name) !== -1) { + this.postis.send({ + method: name, + params: data + }); + } + } + + /** + * Disposes the allocated resources. + * + * @returns {void} + */ + dispose() { + this.postis.destroy(); + } + + /** + * Sends the passed message. + * + * @param {Object} message - The message to be sent. + * @returns {void} + */ + send(message) { + this.postis.send({ + method: POSTIS_METHOD_NAME, + params: message + }); + + if (this._enableLegacyFormat) { + // For the legacy use case we don't need any new fields defined in + // Transport class. That's why we are passing only the original + // object passed by the consumer of the Transport class which is + // message.data. + this._sendLegacyMessage(message.data); + } + } + + /** + * Sets the callback for receiving data. + * + * @param {Function} callback - The new callback. + * @returns {void} + */ + setReceiveCallback(callback) { + this._receiveCallback = callback; + } +} diff --git a/modules/transport/Transport.js b/modules/transport/Transport.js new file mode 100644 index 000000000..678ad3277 --- /dev/null +++ b/modules/transport/Transport.js @@ -0,0 +1,265 @@ +import { + MESSAGE_TYPE_EVENT, + MESSAGE_TYPE_REQUEST, + MESSAGE_TYPE_RESPONSE +} from './constants'; + +/** + * Stores the currnet transport backend that have to be used. Also implements + * request/response mechanism. + */ +export default class Transport { + /** + * Creates new instance. + * + * @param {Object} options - Optional parameters for configuration of the + * transport backend. + */ + constructor({ backend } = {}) { + /** + * Maps an event name and listener that have been added to the Transport + * instance. + * + * @type {Map} + */ + this._listeners = new Map(); + + /** + * The request ID counter used for the id property of the request. This + * property is used to match the responses with the request. + * + * @type {number} + */ + this._requestID = 0; + + /** + * Maps an IDs of the requests and handlers that will process the + * responses of those requests. + * + * @type {Map} + */ + this._responseHandlers = new Map(); + + /** + * A set with the events and requests that were received but not + * processed by any listener. They are later passed on every new + * listener until they are processed. + * + * @type {Set} + */ + this._unprocessedMessages = new Set(); + + /** + * Alias. + */ + this.addListener = this.on; + + if (backend) { + this.setBackend(backend); + } + } + + /** + * Disposes the current transport backend. + * + * @returns {void} + */ + _disposeBackend() { + if (this._backend) { + this._backend.dispose(); + this._backend = null; + } + } + + /** + * Handles incoming messages from the transport backend. + * + * @param {Object} message - The message. + * @returns {void} + */ + _onMessageReceived(message) { + if (message.type === MESSAGE_TYPE_RESPONSE) { + const handler = this._responseHandlers.get(message.id); + + if (handler) { + handler(message); + this._responseHandlers.delete(message.id); + } + } else if (message.type === MESSAGE_TYPE_REQUEST) { + this.emit('request', message.data, (result, error) => { + this._backend.send({ + type: MESSAGE_TYPE_RESPONSE, + error, + id: message.id, + result + }); + }); + } else { + this.emit('event', message.data); + } + } + + /** + * Disposes the allocated resources. + * + * @returns {void} + */ + dispose() { + this._responseHandlers.clear(); + this._unprocessedMessages.clear(); + this.removeAllListeners(); + this._disposeBackend(); + } + + /** + * Calls each of the listeners registered for the event named eventName, in + * the order they were registered, passing the supplied arguments to each. + * + * @param {string} eventName - The name of the event. + * @returns {boolean} True if the event has been processed by any listener, + * false otherwise. + */ + emit(eventName, ...args) { + const listenersForEvent = this._listeners.get(eventName); + let isProcessed = false; + + if (listenersForEvent && listenersForEvent.size) { + listenersForEvent.forEach(listener => { + isProcessed = listener(...args) || isProcessed; + }); + } + + if (!isProcessed) { + this._unprocessedMessages.add(args); + } + + return isProcessed; + } + + /** + * Adds the listener function to the listeners collection for the event + * named eventName. + * + * @param {string} eventName - The name of the event. + * @param {Function} listener - The listener that will be added. + * @returns {Transport} References to the instance of Transport class, so + * that calls can be chained. + */ + on(eventName, listener) { + let listenersForEvent = this._listeners.get(eventName); + + if (!listenersForEvent) { + listenersForEvent = new Set(); + this._listeners.set(eventName, listenersForEvent); + } + + listenersForEvent.add(listener); + + this._unprocessedMessages.forEach(args => { + if (listener(...args)) { + this._unprocessedMessages.delete(args); + } + }); + + return this; + } + + /** + * Removes all listeners, or those of the specified eventName. + * + * @param {string} [eventName] - The name of the event. If this parameter is + * not specified all listeners will be removed. + * @returns {Transport} References to the instance of Transport class, so + * that calls can be chained. + */ + removeAllListeners(eventName) { + if (eventName) { + this._listeners.delete(eventName); + } else { + this._listeners.clear(); + } + + return this; + } + + /** + * Removes the listener function from the listeners collection for the event + * named eventName. + * + * @param {string} eventName - The name of the event. + * @param {Function} listener - The listener that will be removed. + * @returns {Transport} References to the instance of Transport class, so + * that calls can be chained. + */ + removeListener(eventName, listener) { + const listenersForEvent = this._listeners.get(eventName); + + if (listenersForEvent) { + listenersForEvent.delete(listener); + } + + return this; + } + + /** + * Sends the passed event. + * + * @param {Object} event - The event to be sent. + * @returns {void} + */ + sendEvent(event = {}) { + if (this._backend) { + this._backend.send({ + type: MESSAGE_TYPE_EVENT, + data: event + }); + } + } + + /** + * Sending request. + * + * @param {Object} request - The request to be sent. + * @returns {Promise} + */ + sendRequest(request) { + if (!this._backend) { + return Promise.reject(new Error('No transport backend defined!')); + } + + this._requestID++; + + const id = this._requestID; + + return new Promise((resolve, reject) => { + this._responseHandlers.set(id, ({ error, result }) => { + if (result) { + resolve(result); + } else if (error) { + reject(error); + } else { // no response + reject(new Error('Unexpected response format!')); + } + }); + + this._backend.send({ + type: MESSAGE_TYPE_REQUEST, + data: request, + id + }); + }); + } + + /** + * Changes the current backend transport. + * + * @param {Object} backend - The new transport backend that will be used. + * @returns {void} + */ + setBackend(backend) { + this._disposeBackend(); + + this._backend = backend; + this._backend.setReceiveCallback(this._onMessageReceived.bind(this)); + } +} diff --git a/modules/transport/constants.js b/modules/transport/constants.js new file mode 100644 index 000000000..54fb87cf0 --- /dev/null +++ b/modules/transport/constants.js @@ -0,0 +1,20 @@ +/** + * The message type for events. + * + * @type {string} + */ +export const MESSAGE_TYPE_EVENT = 'event'; + +/** + * The message type for requests. + * + * @type {string} + */ +export const MESSAGE_TYPE_REQUEST = 'request'; + +/** + * The message type for responses. + * + * @type {string} + */ +export const MESSAGE_TYPE_RESPONSE = 'response'; diff --git a/modules/transport/index.js b/modules/transport/index.js new file mode 100644 index 000000000..53db0877e --- /dev/null +++ b/modules/transport/index.js @@ -0,0 +1,57 @@ +// FIXME: change to '../API' when we update to webpack2. If we do this now all +// files from API modules will be included in external_api.js. +import { API_ID } from '../API/constants'; +import { getJitsiMeetGlobalNS } from '../util/helpers'; + +import PostMessageTransportBackend from './PostMessageTransportBackend'; +import Transport from './Transport'; + +export { + PostMessageTransportBackend, + Transport +}; + +/** + * Option for the default low level transport. + * + * @type {Object} + */ +const postisOptions = {}; + +if (typeof API_ID === 'number') { + postisOptions.scope = `jitsi_meet_external_api_${API_ID}`; +} + +/** + * The instance of Transport class that will be used by Jitsi Meet. + * + * @type {Transport} + */ +let transport; + +/** + * Returns the instance of Transport class that will be used by Jitsi Meet. + * + * @returns {Transport} + */ +export function getJitsiMeetTransport() { + if (!transport) { + transport = new Transport({ + backend: new PostMessageTransportBackend({ + enableLegacyFormat: true, + postisOptions + }) + }); + } + + return transport; +} + +/** + * Sets the transport to passed transport. + * + * @param {Object} externalTransportBackend - The new transport. + * @returns {void} + */ +getJitsiMeetGlobalNS().setExternalTransportBackend = externalTransportBackend => + transport.setBackend(externalTransportBackend); diff --git a/modules/util/helpers.js b/modules/util/helpers.js index 55408151a..39d173d49 100644 --- a/modules/util/helpers.js +++ b/modules/util/helpers.js @@ -16,35 +16,6 @@ export function createDeferred() { return deferred; } -/** - * Reload page. - */ -export function reload() { - window.location.reload(); -} - -/** - * Redirects to a specific new URL by replacing the current location (in the - * history). - * - * @param {string} url the URL pointing to the location where the user should - * be redirected to. - */ -export function replace(url) { - window.location.replace(url); -} - -/** - * Prints the error and reports it to the global error handler. - * - * @param e {Error} the error - * @param msg {string} [optional] the message printed in addition to the error - */ -export function reportError(e, msg = "") { - logger.error(msg, e); - window.onerror && window.onerror(msg, null, null, null, e); -} - /** * Creates a debounced function that delays invoking func until after wait * milliseconds have elapsed since the last time the debounced function was @@ -74,3 +45,49 @@ export function debounce(fn, wait = 0, options = {}) { } }; } + +/** + * Returns the namespace for all global variables, functions, etc that we need. + * + * @returns {Object} the namespace. + * + * NOTE: After React-ifying everything this should be the only global. + */ +export function getJitsiMeetGlobalNS() { + if (!window.JitsiMeetJS) { + window.JitsiMeetJS = {}; + } + if (!window.JitsiMeetJS.app) { + window.JitsiMeetJS.app = {}; + } + return window.JitsiMeetJS.app; +} + +/** + * Reload page. + */ +export function reload() { + window.location.reload(); +} + +/** + * Redirects to a specific new URL by replacing the current location (in the + * history). + * + * @param {string} url the URL pointing to the location where the user should + * be redirected to. + */ +export function replace(url) { + window.location.replace(url); +} + +/** + * Prints the error and reports it to the global error handler. + * + * @param e {Error} the error + * @param msg {string} [optional] the message printed in addition to the error + */ +export function reportError(e, msg = "") { + logger.error(msg, e); + window.onerror && window.onerror(msg, null, null, null, e); +} diff --git a/react/index.web.js b/react/index.web.js index 785afb653..987e1d4fc 100644 --- a/react/index.web.js +++ b/react/index.web.js @@ -3,6 +3,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { getJitsiMeetTransport } from '../modules/transport'; + import config from './config'; import { App } from './features/app'; @@ -34,4 +36,5 @@ window.addEventListener('beforeunload', () => { APP.logCollectorStarted = false; } APP.API.dispose(); + getJitsiMeetTransport().dispose(); }); diff --git a/service/remotecontrol/Constants.js b/service/remotecontrol/Constants.js index c856185ac..e295614c6 100644 --- a/service/remotecontrol/Constants.js +++ b/service/remotecontrol/Constants.js @@ -37,7 +37,7 @@ export const PERMISSIONS_ACTIONS = { /** * The type of remote control events sent trough the API module. */ -export const REMOTE_CONTROL_EVENT_TYPE = "remote-control-event"; +export const REMOTE_CONTROL_EVENT_NAME = "remote-control-event"; /** * The remote control event.