feat(transport): Implement transport module

The transport module will be handling all external app
communication.
This commit is contained in:
hristoterezov 2017-04-17 10:52:31 -05:00
parent 6c676f8d5f
commit 0dff35c0db
12 changed files with 532 additions and 109 deletions

View File

@ -9,6 +9,8 @@ node_modules/
# supersedes JSHint.
flow-typed/
react/
modules/API/
modules/transport/
# The following are checked by ESLint with the minimum configuration which does
# not supersede JSHint but take advantage of advanced language features such as

2
app.js
View File

@ -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";

View File

@ -1,8 +1,9 @@
/* global APP, getConfigParamsFromUrl */
import postisInit from 'postis';
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import { transport } from '../transport';
import { API_ID } from './constants';
declare var APP: Object;
/**
* List of the available commands.
@ -17,23 +18,6 @@ let commands = {};
*/
let initialScreenSharingState = false;
/**
* JitsiMeetExternalAPI id - unique for a webpage.
*/
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;
/**
* Initializes supported commands.
*
@ -55,8 +39,17 @@ function initCommands() {
'remote-control-event':
event => APP.remoteControl.onRemoteControlAPIEvent(event)
};
Object.keys(commands).forEach(
key => postis.listen(key, args => commands[key](...args)));
transport.on('event', event => {
const { name, data } = event;
if (name && commands[name]) {
commands[name](...data);
return true;
}
return false;
});
}
/**
@ -72,25 +65,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 +87,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
@ -142,42 +108,42 @@ class API {
return;
}
if (!postis) {
APP.conference.addListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
this._initPostis();
}
/**
* Current status (enabled/disabled) of API.
*/
this.enabled = true;
APP.conference.addListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
initCommands();
}
/**
* Initializes postis library.
* Sends message to the external application.
*
* @param {string} name - The name of the event.
* @param {Object} data - The data to be sent.
* @returns {void}
*
* @private
*/
_initPostis() {
const postisOptions = {
window: target
};
if (typeof jitsiMeetExternalApiId === 'number') {
postisOptions.scope
= `jitsi_meet_external_api_${jitsiMeetExternalApiId}`;
_sendEvent(name, data = {}) {
if (this.enabled) {
transport.sendEvent({
name,
data
});
}
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('outgoing-message', { message });
}
/**
@ -194,14 +160,12 @@ class API {
return;
}
triggerEvent(
'incoming-message',
{
'from': id,
'message': body,
'nick': nick,
'stamp': ts
});
this._sendEvent('incoming-message', {
from: id,
nick,
message: body,
stamp: ts
});
}
/**
@ -212,7 +176,7 @@ class API {
* @returns {void}
*/
notifyUserJoined(id) {
triggerEvent('participant-joined', { id });
this._sendEvent('participant-joined', { id });
}
/**
@ -223,7 +187,7 @@ class API {
* @returns {void}
*/
notifyUserLeft(id) {
triggerEvent('participant-left', { id });
this._sendEvent('participant-left', { id });
}
/**
@ -231,39 +195,36 @@ 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('display-name-change', {
id,
displayname
});
}
/**
* 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('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('video-conference-left', { roomName });
}
/**
@ -273,7 +234,7 @@ class API {
* @returns {void}
*/
notifyReadyToClose() {
triggerEvent('video-ready-to-close', {});
this._sendEvent('video-ready-to-close', {});
}
/**
@ -283,22 +244,17 @@ class API {
* @returns {void}
*/
sendRemoteControlEvent(event) {
sendMessage({
method: 'remote-control-event',
params: event
});
this._sendEvent('remote-control-event', 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);

7
modules/API/constants.js Normal file
View File

@ -0,0 +1,7 @@
declare var getConfigParamsFromUrl: Function;
/**
* JitsiMeetExternalAPI id - unique for a webpage.
*/
export const API_ID
= getConfigParamsFromUrl().jitsi_meet_external_api_id;

2
modules/API/index.js Normal file
View File

@ -0,0 +1,2 @@
export default from './API';
export * from './constants';

View File

@ -0,0 +1,3 @@
module.exports = {
'extends': '../../react/.eslintrc.js'
};

View File

@ -0,0 +1,141 @@
import Postis from 'postis';
/**
* The list of methods of incomming postis messages that we have to support for
* backward compatability for the users that are directly sending messages to
* Jitsi Meet (without using external_api.js)
*
* @type {string[]}
*/
const legacyIncomingMethods = [ 'display-name', 'toggle-audio', 'toggle-video',
'toggle-film-strip', 'toggle-chat', 'toggle-contact-list',
'toggle-share-screen', 'video-hangup', 'email', 'avatar-url' ];
/**
* The list of methods of outgoing postis messages that we have to support for
* backward compatability for the users that are directly listening to the
* postis messages send by Jitsi Meet(without using external_api.js).
*
* @type {string[]}
*/
const legacyOutgoingMethods = [ 'display-name-change', 'incoming-message',
'outgoing-message', 'participant-joined', 'participant-left',
'video-ready-to-close', 'video-conference-joined',
'video-conference-left' ];
/**
* The postis method used for all messages.
*
* @type {string}
*/
const POSTIS_METHOD_NAME = 'data';
/**
* The default options for postis.
*
* @type {Object}
*/
const defaultPostisOptions = {
window: window.opener || window.parent
};
/**
* 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(options = {}) {
const postisOptions = Object.assign({}, defaultPostisOptions, options);
this.postis = Postis(postisOptions);
// backward compatability
legacyIncomingMethods.forEach(method =>
this.postis.listen(method,
params => this._onPostisDataReceived(method, params)));
this.postis.listen(POSTIS_METHOD_NAME, data =>
this._dataReceivedCallBack(data));
this._dataReceivedCallBack = () => {
// do nothing until real callback is set;
};
}
/**
* Handles incomming legacy postis data.
*
* @param {string} method - The method property from postis data object.
* @param {Any} params - The params property from postis data object.
* @returns {void}
*/
_onPostisDataReceived(method, params = {}) {
const newData = {
data: {
name: method,
data: params
}
};
this._dataReceivedCallBack(newData);
}
/**
* Sends the passed data via postis using the old format.
*
* @param {Object} data - The data to be sent.
* @returns {void}
*/
_sendLegacyData(data) {
const method = data.name;
if (method && legacyOutgoingMethods.indexOf(method) !== -1) {
this.postis.send({
method,
params: data.data
});
}
}
/**
* Disposes the allocated resources.
*
* @returns {void}
*/
dispose() {
this.postis.destroy();
}
/**
* Sends the passed data.
*
* @param {Object} data - The data to be sent.
* @returns {void}
*/
send(data) {
this.postis.send({
method: POSTIS_METHOD_NAME,
params: data
});
// 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 data.data.
this._sendLegacyData(data.data);
}
/**
* Sets the callback for receiving data.
*
* @param {Function} callback - The new callback.
* @returns {void}
*/
setDataReceivedCallback(callback) {
this._dataReceivedCallBack = callback;
}
}

View File

@ -0,0 +1,243 @@
import {
MESSAGE_TYPE_EVENT,
MESSAGE_TYPE_RESPONSE,
MESSAGE_TYPE_REQUEST
} from './constants';
/**
* Stores the currnet transport that have to be used.
*/
export default class Transport {
/**
* Creates new instance.
*
* @param {Object} options - Optional parameters for configuration of the
* transport.
*/
constructor(options = {}) {
const { transport } = options;
this._requestID = 0;
this._responseHandlers = new Map();
this._listeners = new Map();
this._unprocessedMessages = new Set();
this.addListener = this.on;
if (transport) {
this.setTransport(transport);
}
}
/**
* Disposes the current transport.
*
* @returns {void}
*/
_disposeTransport() {
if (this._transport) {
this._transport.dispose();
this._transport = null;
}
}
/**
* Handles incomming data from the transport.
*
* @param {Object} data - The data.
* @returns {void}
*/
_onDataReceived(data) {
if (data.type === MESSAGE_TYPE_RESPONSE) {
const handler = this._responseHandlers.get(data.id);
if (handler) {
handler(data);
this._responseHandlers.delete(data.id);
}
return;
}
if (data.type === MESSAGE_TYPE_REQUEST) {
this.emit('request', data.data, (result, error) => {
this._transport.send({
type: MESSAGE_TYPE_RESPONSE,
result,
error,
id: data.id
});
});
} else {
this.emit('event', data.data);
}
}
/**
* Disposes the allocated resources.
*
* @returns {void}
*/
dispose() {
this._responseHandlers.clear();
this._unprocessedMessages.clear();
this.removeAllListeners();
this._disposeTransport();
}
/**
* 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 had listeners, false otherwise.
*/
emit(eventName, ...args) {
const listenersForEvent = this._listeners.get(eventName);
if (!listenersForEvent || listenersForEvent.size === 0) {
this._unprocessedMessages.add(args);
return false;
}
let isProcessed = false;
listenersForEvent.forEach(listener => {
isProcessed = listener(...args) || isProcessed;
});
if (!isProcessed) {
this._unprocessedMessages.add(args);
}
}
/**
* 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.
* @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 data.
*
* @param {Object} data - The data to be sent.
* @returns {void}
*/
sendEvent(data = {}) {
if (this._transport) {
this._transport.send({
type: MESSAGE_TYPE_EVENT,
data
});
}
}
/**
* Sending request.
*
* @param {Object} data - The data for the request.
* @returns {Promise}
*/
sendRequest(data) {
if (!this._transport) {
return Promise.reject(new Error('No transport defined!'));
}
this._requestID++;
const id = this._requestID;
return new Promise((resolve, reject) => {
this._responseHandlers.set(this._requestID, response => {
const { result, error } = response;
if (result) {
resolve(result);
} else if (error) {
reject(error);
} else { // no response
reject(new Error('Unexpected response format!'));
}
});
this._transport.send({
id,
type: MESSAGE_TYPE_REQUEST,
data
});
});
}
/**
* Changes the current transport.
*
* @param {Object} transport - The new transport that will be used.
* @returns {void}
*/
setTransport(transport) {
this._disposeTransport();
this._transport = transport;
this._transport.setDataReceivedCallback(
this._onDataReceived.bind(this));
}
}

View File

@ -0,0 +1,20 @@
/**
* The message type for events.
*
* @type {string}
*/
export const MESSAGE_TYPE_EVENT = 'event';
/**
* The message type for responses.
*
* @type {string}
*/
export const MESSAGE_TYPE_RESPONSE = 'response';
/**
* The message type for requests.
*
* @type {string}
*/
export const MESSAGE_TYPE_REQUEST = 'request';

View File

@ -0,0 +1,31 @@
import { API_ID } from '../API';
import { getJitsiMeetGlobalNS } from '../util/helpers';
import Transport from './Transport';
import PostMessageTransportBackend from './PostMessageTransportBackend';
/**
* Option for the default low level transport.
*
* @type {Object}
*/
const postMessageOptions = {};
if (typeof API_ID === 'number') {
postMessageOptions.scope
= `jitsi_meet_external_api_${API_ID}`;
}
export const transport = new Transport({
transport: new PostMessageTransportBackend(postMessageOptions)
});
/**
* Sets the transport to passed transport.
*
* @param {Object} newTransport - The new transport.
* @returns {void}
*/
getJitsiMeetGlobalNS().useNewExternalTransport = function(newTransport) {
transport.setTransport(newTransport);
};

View File

@ -74,3 +74,18 @@ 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 reactifying everything this should be the only place where
* we store everything that needs to be global (for some reason).
*/
export function getJitsiMeetGlobalNS() {
if(!window.JitsiMeetGlobalNS) {
window.JitsiMeetGlobalNS = { };
}
return window.JitsiMeetGlobalNS;
}

View File

@ -3,6 +3,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { transport } 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();
transport.dispose();
});