Merge branch 'abstract_transport2'

Additionally, attempt to move closer to the coding style adopted by
react/.
This commit is contained in:
Lyubo Marinov 2017-05-02 17:39:36 -05:00
commit 75a486ff96
19 changed files with 843 additions and 285 deletions

View File

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

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

@ -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) {

View File

@ -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) {
/**
* Current status (enabled/disabled) of API.
*
* @private
* @type {boolean}
*/
this._enabled = true;
APP.conference.addListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
this._initPostis();
}
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,20 +158,17 @@ 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,14 +205,13 @@ 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,
notifyDisplayNameChanged(id, displayname) {
this._sendEvent({
name: 'display-name-change',
displayname,
id
});
}
@ -247,23 +220,28 @@ class API {
* 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);

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

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

View File

@ -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({
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.postis.listen(postisMethod,
(...args) => this.emit(eventName, ...args));
this._transport.on('event', ({ name, ...data }) => {
if (name === 'participant-joined') {
changeParticipantNumber(this, 1);
} else if (name === 'participant-left') {
changeParticipantNumber(this, -1);
}
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]
});
}

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

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

View File

@ -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
if(this.enabled
&& event.name === REMOTE_CONTROL_EVENT_NAME
&& event.type === EVENT_TYPES.permissions
&& userId === this.requestedParticipant) {
if(remoteControlEvent.action !== PERMISSIONS_ACTIONS.grant) {
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,13 +169,14 @@ 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
if(this.enabled
&& event.name === REMOTE_CONTROL_EVENT_NAME
&& event.type === EVENT_TYPES.stop
&& participant.getId() === this.controlledParticipant) {
this._stop();
}

View File

@ -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);
}
}

View File

@ -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
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);
}
}

View File

@ -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);
}

View File

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

View File

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

View File

@ -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<string, Function>}
*/
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<number, Function>}
*/
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<Object>}
*/
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));
}
}

View File

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

View File

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

View File

@ -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);
}

View File

@ -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();
});

View File

@ -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.