Merge branch 'jitsi-meet-new'
This commit is contained in:
commit
c0dde18e6b
|
@ -0,0 +1,10 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
max_line_length = 80
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -1 +1,2 @@
|
||||||
*.bundle.js -text -diff
|
*.bundle.js -text -diff
|
||||||
|
lib-jitsi-meet.js -text -diff
|
||||||
|
|
|
@ -2,7 +2,10 @@ node_modules
|
||||||
libs
|
libs
|
||||||
debian
|
debian
|
||||||
analytics.js
|
analytics.js
|
||||||
|
lib-jitsi-meet.js
|
||||||
|
|
||||||
modules/xmpp/strophe.emuc.js
|
modules/xmpp/strophe.emuc.js
|
||||||
modules/UI/prezi/Prezi.js
|
modules/UI/prezi/Prezi.js
|
||||||
modules/RTC/adapter.screenshare.js
|
modules/RTC/adapter.screenshare.js
|
||||||
|
modules/statistics/*
|
||||||
|
modules/UI/videolayout/*
|
||||||
|
|
|
@ -15,5 +15,6 @@
|
||||||
"newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
"newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||||
"maxlen": 80, // {int} Max number of characters per line
|
"maxlen": 80, // {int} Max number of characters per line
|
||||||
"latedef": false, //This option prohibits the use of a variable before it was defined
|
"latedef": false, //This option prohibits the use of a variable before it was defined
|
||||||
"laxbreak": true //Ignore line breaks around "=", "==", "&&", etc.
|
"laxbreak": true, //Ignore line breaks around "=", "==", "&&", etc.
|
||||||
|
"esnext": true //support ES2015
|
||||||
}
|
}
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -8,10 +8,13 @@ DEPLOY_DIR = libs
|
||||||
BROWSERIFY_FLAGS = -d
|
BROWSERIFY_FLAGS = -d
|
||||||
OUTPUT_DIR = .
|
OUTPUT_DIR = .
|
||||||
|
|
||||||
all: compile uglify deploy clean
|
all: update-deps compile uglify deploy clean
|
||||||
|
|
||||||
|
update-deps:
|
||||||
|
$(NPM) update
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
$(NPM) update && $(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js
|
$(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(OUTPUT_DIR)/app.bundle.*
|
rm -f $(OUTPUT_DIR)/app.bundle.*
|
||||||
|
|
112
app.js
112
app.js
|
@ -1,48 +1,99 @@
|
||||||
/* jshint -W117 */
|
/* global $, JitsiMeetJS, config */
|
||||||
/* application specific logic */
|
/* application specific logic */
|
||||||
|
|
||||||
require("jquery");
|
import "babel-polyfill";
|
||||||
require("jquery-ui");
|
import "jquery";
|
||||||
require("strophe");
|
import "jquery-ui";
|
||||||
require("strophe-disco");
|
import "strophe";
|
||||||
require("strophe-caps");
|
import "strophe-disco";
|
||||||
require("tooltip");
|
import "strophe-caps";
|
||||||
require("popover");
|
import "tooltip";
|
||||||
|
import "popover";
|
||||||
|
import "jQuery-Impromptu";
|
||||||
|
import "autosize";
|
||||||
window.toastr = require("toastr");
|
window.toastr = require("toastr");
|
||||||
require("jQuery-Impromptu");
|
|
||||||
require("autosize");
|
|
||||||
|
|
||||||
var APP =
|
import URLProcessor from "./modules/config/URLProcessor";
|
||||||
{
|
import RoomnameGenerator from './modules/util/RoomnameGenerator';
|
||||||
init: function () {
|
|
||||||
this.UI = require("./modules/UI/UI");
|
import UI from "./modules/UI/UI";
|
||||||
this.API = require("./modules/API/API");
|
import statistics from "./modules/statistics/statistics";
|
||||||
|
import settings from "./modules/settings/Settings";
|
||||||
|
import conference from './conference';
|
||||||
|
import API from './modules/API/API';
|
||||||
|
|
||||||
|
import UIEvents from './service/UI/UIEvents';
|
||||||
|
|
||||||
|
|
||||||
|
function buildRoomName () {
|
||||||
|
let path = window.location.pathname;
|
||||||
|
let roomName;
|
||||||
|
|
||||||
|
// determinde the room node from the url
|
||||||
|
// TODO: just the roomnode or the whole bare jid?
|
||||||
|
if (config.getroomnode && typeof config.getroomnode === 'function') {
|
||||||
|
// custom function might be responsible for doing the pushstate
|
||||||
|
roomName = config.getroomnode(path);
|
||||||
|
} else {
|
||||||
|
/* fall back to default strategy
|
||||||
|
* this is making assumptions about how the URL->room mapping happens.
|
||||||
|
* It currently assumes deployment at root, with a rewrite like the
|
||||||
|
* following one (for nginx):
|
||||||
|
location ~ ^/([a-zA-Z0-9]+)$ {
|
||||||
|
rewrite ^/(.*)$ / break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (path.length > 1) {
|
||||||
|
roomName = path.substr(1).toLowerCase();
|
||||||
|
} else {
|
||||||
|
let word = RoomnameGenerator.generateRoomWithoutSeparator();
|
||||||
|
roomName = word.toLowerCase();
|
||||||
|
window.history.pushState(
|
||||||
|
'VideoChat', `Room: ${word}`, window.location.pathname + word
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP = {
|
||||||
|
UI,
|
||||||
|
statistics,
|
||||||
|
settings,
|
||||||
|
conference,
|
||||||
|
API,
|
||||||
|
init () {
|
||||||
this.connectionquality =
|
this.connectionquality =
|
||||||
require("./modules/connectionquality/connectionquality");
|
require("./modules/connectionquality/connectionquality");
|
||||||
this.statistics = require("./modules/statistics/statistics");
|
|
||||||
this.RTC = require("./modules/RTC/RTC");
|
|
||||||
this.desktopsharing =
|
this.desktopsharing =
|
||||||
require("./modules/desktopsharing/desktopsharing");
|
require("./modules/desktopsharing/desktopsharing");
|
||||||
this.xmpp = require("./modules/xmpp/xmpp");
|
|
||||||
this.keyboardshortcut =
|
this.keyboardshortcut =
|
||||||
require("./modules/keyboardshortcut/keyboardshortcut");
|
require("./modules/keyboardshortcut/keyboardshortcut");
|
||||||
this.translation = require("./modules/translation/translation");
|
this.translation = require("./modules/translation/translation");
|
||||||
this.settings = require("./modules/settings/Settings");
|
|
||||||
//this.DTMF = require("./modules/DTMF/DTMF");
|
|
||||||
this.members = require("./modules/members/MemberList");
|
|
||||||
this.configFetch = require("./modules/config/HttpConfigFetch");
|
this.configFetch = require("./modules/config/HttpConfigFetch");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
var isUIReady = APP.UI.start();
|
||||||
|
if (isUIReady) {
|
||||||
|
APP.conference.init({roomName: buildRoomName()}).then(function () {
|
||||||
|
APP.UI.initConference();
|
||||||
|
|
||||||
APP.desktopsharing.init();
|
APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {
|
||||||
APP.RTC.start();
|
APP.translation.setLanguage(language);
|
||||||
APP.xmpp.start();
|
APP.settings.setLanguage(language);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.desktopsharing.init(JitsiMeetJS.isDesktopSharingEnabled());
|
||||||
APP.statistics.start();
|
APP.statistics.start();
|
||||||
APP.connectionquality.init();
|
APP.connectionquality.init();
|
||||||
APP.keyboardshortcut.init();
|
APP.keyboardshortcut.init();
|
||||||
APP.members.start();
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +105,7 @@ function init() {
|
||||||
* will be displayed to the user.
|
* will be displayed to the user.
|
||||||
*/
|
*/
|
||||||
function obtainConfigAndInit() {
|
function obtainConfigAndInit() {
|
||||||
var roomName = APP.UI.getRoomNode();
|
let roomName = APP.conference.roomName;
|
||||||
|
|
||||||
if (config.configLocation) {
|
if (config.configLocation) {
|
||||||
APP.configFetch.obtainConfig(
|
APP.configFetch.obtainConfig(
|
||||||
|
@ -84,23 +135,18 @@ function obtainConfigAndInit() {
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
console.log("(TIME) document ready:\t", window.performance.now());
|
console.log("(TIME) document ready:\t", window.performance.now());
|
||||||
|
|
||||||
var URLProcessor = require("./modules/config/URLProcessor");
|
|
||||||
URLProcessor.setConfigParametersFromUrl();
|
URLProcessor.setConfigParametersFromUrl();
|
||||||
APP.init();
|
APP.init();
|
||||||
|
|
||||||
APP.translation.init();
|
APP.translation.init(settings.getLanguage());
|
||||||
|
|
||||||
if(APP.API.isEnabled())
|
|
||||||
APP.API.init();
|
APP.API.init();
|
||||||
|
|
||||||
APP.UI.start(obtainConfigAndInit);
|
obtainConfigAndInit();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).bind('beforeunload', function () {
|
$(window).bind('beforeunload', function () {
|
||||||
if(APP.API.isEnabled())
|
|
||||||
APP.API.dispose();
|
APP.API.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = APP;
|
module.exports = APP;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,820 @@
|
||||||
|
/* global $, APP, JitsiMeetJS, config, interfaceConfig */
|
||||||
|
import {openConnection} from './connection';
|
||||||
|
//FIXME:
|
||||||
|
import createRoomLocker from './modules/UI/authentication/RoomLocker';
|
||||||
|
//FIXME:
|
||||||
|
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||||
|
|
||||||
|
import CQEvents from './service/connectionquality/CQEvents';
|
||||||
|
import UIEvents from './service/UI/UIEvents';
|
||||||
|
import DSEvents from './service/desktopsharing/DesktopSharingEventTypes';
|
||||||
|
|
||||||
|
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||||
|
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||||
|
|
||||||
|
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||||
|
const ConferenceErrors = JitsiMeetJS.errors.conference;
|
||||||
|
|
||||||
|
let room, connection, localTracks, localAudio, localVideo, roomLocker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known custom conference commands.
|
||||||
|
*/
|
||||||
|
const Commands = {
|
||||||
|
CONNECTION_QUALITY: "stats",
|
||||||
|
EMAIL: "email",
|
||||||
|
VIDEO_TYPE: "videoType",
|
||||||
|
ETHERPAD: "etherpad",
|
||||||
|
PREZI: "prezi",
|
||||||
|
STOP_PREZI: "stop-prezi"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open Connection. When authentication failed it shows auth dialog.
|
||||||
|
* @returns Promise<JitsiConnection>
|
||||||
|
*/
|
||||||
|
function connect() {
|
||||||
|
return openConnection({retry: true}).catch(function (err) {
|
||||||
|
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||||
|
APP.UI.notifyTokenAuthFailed();
|
||||||
|
} else {
|
||||||
|
APP.UI.notifyConnectionFailed(err);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add local track to the conference and shares
|
||||||
|
* video type with other users if its video track.
|
||||||
|
* @param {JitsiLocalTrack} track local track
|
||||||
|
*/
|
||||||
|
function addTrack (track) {
|
||||||
|
room.addTrack(track);
|
||||||
|
|
||||||
|
if (track.isAudioTrack()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.removeCommand(Commands.VIDEO_TYPE);
|
||||||
|
room.sendCommand(Commands.VIDEO_TYPE, {
|
||||||
|
value: track.videoType,
|
||||||
|
attributes: {
|
||||||
|
xmlns: 'http://jitsi.org/jitmeet/video'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share email with other users.
|
||||||
|
* @param {string} email new email
|
||||||
|
*/
|
||||||
|
function sendEmail (email) {
|
||||||
|
room.sendCommand(Commands.EMAIL, {
|
||||||
|
value: email,
|
||||||
|
attributes: {
|
||||||
|
id: room.myUserId()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user nickname by user id.
|
||||||
|
* @param {string} id user id
|
||||||
|
* @returns {string?} user nickname or undefined if user is unknown.
|
||||||
|
*/
|
||||||
|
function getDisplayName (id) {
|
||||||
|
if (APP.conference.isLocalId(id)) {
|
||||||
|
return APP.settings.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
let participant = room.getParticipantById(id);
|
||||||
|
if (participant && participant.getDisplayName()) {
|
||||||
|
return participant.getDisplayName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConferenceConnector {
|
||||||
|
constructor(resolve, reject) {
|
||||||
|
this._resolve = resolve;
|
||||||
|
this._reject = reject;
|
||||||
|
this.reconnectTimeout = null;
|
||||||
|
room.on(ConferenceEvents.CONFERENCE_JOINED,
|
||||||
|
this._handleConferenceJoined.bind(this));
|
||||||
|
room.on(ConferenceEvents.CONFERENCE_FAILED,
|
||||||
|
this._onConferenceFailed.bind(this));
|
||||||
|
room.on(ConferenceEvents.CONFERENCE_ERROR,
|
||||||
|
this._onConferenceError.bind(this));
|
||||||
|
}
|
||||||
|
_handleConferenceFailed(err, msg) {
|
||||||
|
this._unsubscribe();
|
||||||
|
this._reject(err);
|
||||||
|
}
|
||||||
|
_onConferenceFailed(err, ...params) {
|
||||||
|
console.error('CONFERENCE FAILED:', err, params);
|
||||||
|
switch (err) {
|
||||||
|
// room is locked by the password
|
||||||
|
case ConferenceErrors.PASSWORD_REQUIRED:
|
||||||
|
APP.UI.markRoomLocked(true);
|
||||||
|
roomLocker.requirePassword().then(function () {
|
||||||
|
room.join(roomLocker.password);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.CONNECTION_ERROR:
|
||||||
|
{
|
||||||
|
let [msg] = params;
|
||||||
|
APP.UI.notifyConnectionFailed(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
|
||||||
|
APP.UI.notifyBridgeDown();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// not enough rights to create conference
|
||||||
|
case ConferenceErrors.AUTHENTICATION_REQUIRED:
|
||||||
|
// schedule reconnect to check if someone else created the room
|
||||||
|
this.reconnectTimeout = setTimeout(function () {
|
||||||
|
room.join();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// notify user that auth is required
|
||||||
|
AuthHandler.requireAuth(APP.conference.roomName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.RESERVATION_ERROR:
|
||||||
|
{
|
||||||
|
let [code, msg] = params;
|
||||||
|
APP.UI.notifyReservationError(code, msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.GRACEFUL_SHUTDOWN:
|
||||||
|
APP.UI.notifyGracefulShudown();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.JINGLE_FATAL_ERROR:
|
||||||
|
APP.UI.notifyInternalError();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.CONFERENCE_DESTROYED:
|
||||||
|
{
|
||||||
|
let [reason] = params;
|
||||||
|
APP.UI.notifyConferenceDestroyed(reason);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConferenceErrors.FOCUS_DISCONNECTED:
|
||||||
|
{
|
||||||
|
let [focus, retrySec] = params;
|
||||||
|
APP.UI.notifyFocusDisconnected(focus, retrySec);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this._handleConferenceFailed(err, ...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onConferenceError(err, ...params) {
|
||||||
|
console.error('CONFERENCE Error:', err, params);
|
||||||
|
switch (err) {
|
||||||
|
case ConferenceErrors.CHAT_ERROR:
|
||||||
|
{
|
||||||
|
let [code, msg] = params;
|
||||||
|
APP.UI.showChatError(code, msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknown error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_unsubscribe() {
|
||||||
|
room.off(
|
||||||
|
ConferenceEvents.CONFERENCE_JOINED, this._handleConferenceJoined);
|
||||||
|
room.off(
|
||||||
|
ConferenceEvents.CONFERENCE_FAILED, this._onConferenceFailed);
|
||||||
|
if (this.reconnectTimeout !== null) {
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
}
|
||||||
|
AuthHandler.closeAuth();
|
||||||
|
}
|
||||||
|
_handleConferenceJoined() {
|
||||||
|
this._unsubscribe();
|
||||||
|
this._resolve();
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
room.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
localId: undefined,
|
||||||
|
isModerator: false,
|
||||||
|
audioMuted: false,
|
||||||
|
videoMuted: false,
|
||||||
|
/**
|
||||||
|
* Open new connection and join to the conference.
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} roomName name of the conference
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
init(options) {
|
||||||
|
this.roomName = options.roomName;
|
||||||
|
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE);
|
||||||
|
|
||||||
|
return JitsiMeetJS.init(config).then(() => {
|
||||||
|
return Promise.all([
|
||||||
|
this.createLocalTracks('audio', 'video').catch(
|
||||||
|
() => {return [];}),
|
||||||
|
connect()
|
||||||
|
]);
|
||||||
|
}).then(([tracks, con]) => {
|
||||||
|
console.log('initialized with %s local tracks', tracks.length);
|
||||||
|
localTracks = tracks;
|
||||||
|
connection = con;
|
||||||
|
this._createRoom();
|
||||||
|
// XXX The API will take care of disconnecting from the XMPP server
|
||||||
|
// (and, thus, leaving the room) on unload.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
(new ConferenceConnector(resolve, reject)).connect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Create local tracks of specified types.
|
||||||
|
* If we cannot obtain required tracks it will return empty array.
|
||||||
|
* @param {string[]} devices required track types ('audio', 'video' etc.)
|
||||||
|
* @returns {Promise<JitsiLocalTrack[]>}
|
||||||
|
*/
|
||||||
|
createLocalTracks (...devices) {
|
||||||
|
return JitsiMeetJS.createLocalTracks({
|
||||||
|
// copy array to avoid mutations inside library
|
||||||
|
devices: devices.slice(0),
|
||||||
|
resolution: config.resolution,
|
||||||
|
// adds any ff fake device settings if any
|
||||||
|
firefox_fake_device: config.firefox_fake_device
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('failed to create local tracks', ...devices, err);
|
||||||
|
APP.statistics.onGetUserMediaFailed(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if id is id of the local user.
|
||||||
|
* @param {string} id id to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isLocalId (id) {
|
||||||
|
return this.localId === id;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
||||||
|
* @param mute true for mute and false for unmute.
|
||||||
|
*/
|
||||||
|
muteAudio (mute) {
|
||||||
|
//FIXME: Maybe we should create method for that in the UI instead of
|
||||||
|
//accessing directly eventEmitter????
|
||||||
|
APP.UI.eventEmitter.emit(UIEvents.AUDIO_MUTED, mute);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
||||||
|
*/
|
||||||
|
toggleAudioMuted () {
|
||||||
|
this.muteAudio(!this.audioMuted);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
||||||
|
* @param mute true for mute and false for unmute.
|
||||||
|
*/
|
||||||
|
muteVideo (mute) {
|
||||||
|
//FIXME: Maybe we should create method for that in the UI instead of
|
||||||
|
//accessing directly eventEmitter????
|
||||||
|
APP.UI.eventEmitter.emit(UIEvents.VIDEO_MUTED, mute);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
||||||
|
*/
|
||||||
|
toggleVideoMuted () {
|
||||||
|
this.muteVideo(!this.videoMuted);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Retrieve list of conference participants (without local user).
|
||||||
|
* @returns {JitsiParticipant[]}
|
||||||
|
*/
|
||||||
|
listMembers () {
|
||||||
|
return room.getParticipants();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Retrieve list of ids of conference participants (without local user).
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
listMembersIds () {
|
||||||
|
return room.getParticipants().map(p => p.getId());
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if SIP is supported.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
sipGatewayEnabled () {
|
||||||
|
return room.isSIPCallingSupported();
|
||||||
|
},
|
||||||
|
get membersCount () {
|
||||||
|
return room.getParticipants().length + 1;
|
||||||
|
},
|
||||||
|
get startAudioMuted () {
|
||||||
|
return room && room.getStartMutedPolicy().audio;
|
||||||
|
},
|
||||||
|
get startVideoMuted () {
|
||||||
|
return room && room.getStartMutedPolicy().video;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns true if the callstats integration is enabled, otherwise returns
|
||||||
|
* false.
|
||||||
|
*
|
||||||
|
* @returns true if the callstats integration is enabled, otherwise returns
|
||||||
|
* false.
|
||||||
|
*/
|
||||||
|
isCallstatsEnabled () {
|
||||||
|
return room.isCallstatsEnabled();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Sends the given feedback through CallStats if enabled.
|
||||||
|
*
|
||||||
|
* @param overallFeedback an integer between 1 and 5 indicating the
|
||||||
|
* user feedback
|
||||||
|
* @param detailedFeedback detailed feedback from the user. Not yet used
|
||||||
|
*/
|
||||||
|
sendFeedback (overallFeedback, detailedFeedback) {
|
||||||
|
return room.sendFeedback (overallFeedback, detailedFeedback);
|
||||||
|
},
|
||||||
|
// used by torture currently
|
||||||
|
isJoined () {
|
||||||
|
return this._room
|
||||||
|
&& this._room.isJoined();
|
||||||
|
},
|
||||||
|
getConnectionState () {
|
||||||
|
return this._room
|
||||||
|
&& this._room.getConnectionState();
|
||||||
|
},
|
||||||
|
getMyUserId () {
|
||||||
|
return this._room
|
||||||
|
&& this._room.myUserId();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Will be filled with values only when config.debug is enabled.
|
||||||
|
* Its used by torture to check audio levels.
|
||||||
|
*/
|
||||||
|
audioLevelsMap: {},
|
||||||
|
getPeerSSRCAudioLevel (id) {
|
||||||
|
return this.audioLevelsMap[id];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Will check for number of remote particiapnts that have at least one
|
||||||
|
* remote track.
|
||||||
|
* @return {boolean} whether we have enough participants with remote streams
|
||||||
|
*/
|
||||||
|
checkEnoughParticipants (number) {
|
||||||
|
var participants = this._room.getParticipants();
|
||||||
|
|
||||||
|
var foundParticipants = 0;
|
||||||
|
for (var i = 0; i < participants.length; i += 1) {
|
||||||
|
if (participants[i].getTracks().length > 0) {
|
||||||
|
foundParticipants++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundParticipants >= number;
|
||||||
|
},
|
||||||
|
// end used by torture
|
||||||
|
|
||||||
|
getLogs () {
|
||||||
|
return room.getLogs();
|
||||||
|
},
|
||||||
|
_createRoom () {
|
||||||
|
room = connection.initJitsiConference(APP.conference.roomName,
|
||||||
|
this._getConferenceOptions());
|
||||||
|
this.localId = room.myUserId();
|
||||||
|
localTracks.forEach((track) => {
|
||||||
|
if(track.isAudioTrack()) {
|
||||||
|
localAudio = track;
|
||||||
|
}
|
||||||
|
else if (track.isVideoTrack()) {
|
||||||
|
localVideo = track;
|
||||||
|
}
|
||||||
|
addTrack(track);
|
||||||
|
APP.UI.addLocalStream(track);
|
||||||
|
});
|
||||||
|
roomLocker = createRoomLocker(room);
|
||||||
|
this._room = room; // FIXME do not use this
|
||||||
|
this.localId = room.myUserId();
|
||||||
|
|
||||||
|
let email = APP.settings.getEmail();
|
||||||
|
email && sendEmail(email);
|
||||||
|
|
||||||
|
let nick = APP.settings.getDisplayName();
|
||||||
|
(config.useNicks && !nick) && (() => {
|
||||||
|
nick = APP.UI.askForNickname();
|
||||||
|
APP.settings.setDisplayName(nick);
|
||||||
|
})();
|
||||||
|
nick && room.setDisplayName(nick);
|
||||||
|
|
||||||
|
this._setupListeners();
|
||||||
|
},
|
||||||
|
_getConferenceOptions() {
|
||||||
|
let options = config;
|
||||||
|
if(config.enableRecording) {
|
||||||
|
options.recordingType = (config.hosts &&
|
||||||
|
(typeof config.hosts.jirecon != "undefined"))?
|
||||||
|
"jirecon" : "colibri";
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Setup interaction between conference and UI.
|
||||||
|
*/
|
||||||
|
_setupListeners () {
|
||||||
|
// add local streams when joined to the conference
|
||||||
|
room.on(ConferenceEvents.CONFERENCE_JOINED, () => {
|
||||||
|
APP.UI.updateAuthInfo(room.isAuthEnabled(), room.getAuthLogin());
|
||||||
|
APP.UI.mucJoined();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.USER_JOINED, (id, user) => {
|
||||||
|
console.log('USER %s connnected', id, user);
|
||||||
|
APP.API.notifyUserJoined(id);
|
||||||
|
// FIXME email???
|
||||||
|
APP.UI.addUser(id, user.getDisplayName());
|
||||||
|
|
||||||
|
// chek the roles for the new user and reflect them
|
||||||
|
APP.UI.updateUserRole(user);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.USER_LEFT, (id, user) => {
|
||||||
|
console.log('USER %s LEFT', id, user);
|
||||||
|
APP.API.notifyUserLeft(id);
|
||||||
|
APP.UI.removeUser(id, user.getDisplayName());
|
||||||
|
APP.UI.stopPrezi(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
|
||||||
|
if (this.isLocalId(id)) {
|
||||||
|
console.info(`My role changed, new role: ${role}`);
|
||||||
|
this.isModerator = room.isModerator();
|
||||||
|
APP.UI.updateLocalRole(room.isModerator());
|
||||||
|
} else {
|
||||||
|
let user = room.getParticipantById(id);
|
||||||
|
if (user) {
|
||||||
|
APP.UI.updateUserRole(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.TRACK_ADDED, (track) => {
|
||||||
|
if(!track || track.isLocal())
|
||||||
|
return;
|
||||||
|
APP.UI.addRemoteStream(track);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.TRACK_REMOVED, (track) => {
|
||||||
|
// FIXME handle
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.TRACK_MUTE_CHANGED, (track) => {
|
||||||
|
if(!track)
|
||||||
|
return;
|
||||||
|
const handler = (track.getType() === "audio")?
|
||||||
|
APP.UI.setAudioMuted : APP.UI.setVideoMuted;
|
||||||
|
let id;
|
||||||
|
const mute = track.isMuted();
|
||||||
|
if(track.isLocal()){
|
||||||
|
id = this.localId;
|
||||||
|
if(track.getType() === "audio") {
|
||||||
|
this.audioMuted = mute;
|
||||||
|
} else {
|
||||||
|
this.videoMuted = mute;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = track.getParticipantId();
|
||||||
|
}
|
||||||
|
handler(id , mute);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, (id, lvl) => {
|
||||||
|
if(this.isLocalId(id) && localAudio.isMuted()) {
|
||||||
|
lvl = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(config.debug)
|
||||||
|
this.audioLevelsMap[id] = lvl;
|
||||||
|
|
||||||
|
APP.UI.setAudioLevel(id, lvl);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.IN_LAST_N_CHANGED, (inLastN) => {
|
||||||
|
//FIXME
|
||||||
|
if (config.muteLocalVideoIfNotInLastN) {
|
||||||
|
// TODO mute or unmute if required
|
||||||
|
// mark video on UI
|
||||||
|
// APP.UI.markVideoMuted(true/false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
room.on(
|
||||||
|
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
|
||||||
|
APP.UI.handleLastNEndpoints(ids, enteringIds);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
|
||||||
|
APP.UI.markDominantSpeaker(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!interfaceConfig.filmStripOnly) {
|
||||||
|
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||||
|
APP.UI.markVideoInterrupted(true);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
|
||||||
|
APP.UI.markVideoInterrupted(false);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.MESSAGE_RECEIVED, (id, text, ts) => {
|
||||||
|
let nick = getDisplayName(id);
|
||||||
|
APP.API.notifyReceivedChatMessage(id, nick, text, ts);
|
||||||
|
APP.UI.addMessage(id, nick, text, ts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
|
||||||
|
APP.API.notifyDisplayNameChanged(id, displayName);
|
||||||
|
APP.UI.changeDisplayName(id, displayName);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.RECORDING_STATE_CHANGED, (status, error) => {
|
||||||
|
if(status == "error") {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
APP.UI.updateRecordingState(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
|
||||||
|
APP.UI.updateUserStatus(id, status);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.KICKED, () => {
|
||||||
|
APP.UI.notifyKicked();
|
||||||
|
// FIXME close
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.DTMF_SUPPORT_CHANGED, (isDTMFSupported) => {
|
||||||
|
APP.UI.updateDTMFSupport(isDTMFSupported);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(ConferenceEvents.FIREFOX_EXTENSION_NEEDED, function (url) {
|
||||||
|
APP.UI.notifyFirefoxExtensionRequired(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.ROOM_LOCK_CLICKED, () => {
|
||||||
|
if (room.isModerator()) {
|
||||||
|
let promise = roomLocker.isLocked
|
||||||
|
? roomLocker.askToUnlock()
|
||||||
|
: roomLocker.askToLock();
|
||||||
|
promise.then(() => {
|
||||||
|
APP.UI.markRoomLocked(roomLocker.isLocked);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
roomLocker.notifyModeratorRequired();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.AUDIO_MUTED, (muted) => {
|
||||||
|
(muted)? localAudio.mute() : localAudio.unmute();
|
||||||
|
});
|
||||||
|
APP.UI.addListener(UIEvents.VIDEO_MUTED, (muted) => {
|
||||||
|
(muted)? localVideo.mute() : localVideo.unmute();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!interfaceConfig.filmStripOnly) {
|
||||||
|
APP.UI.addListener(UIEvents.MESSAGE_CREATED, (message) => {
|
||||||
|
APP.API.notifySendingChatMessage(message);
|
||||||
|
room.sendTextMessage(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
APP.connectionquality.addListener(
|
||||||
|
CQEvents.LOCALSTATS_UPDATED,
|
||||||
|
(percent, stats) => {
|
||||||
|
APP.UI.updateLocalStats(percent, stats);
|
||||||
|
|
||||||
|
// send local stats to other users
|
||||||
|
room.sendCommandOnce(Commands.CONNECTION_QUALITY, {
|
||||||
|
children: APP.connectionquality.convertToMUCStats(stats),
|
||||||
|
attributes: {
|
||||||
|
xmlns: 'http://jitsi.org/jitmeet/stats'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
APP.connectionquality.addListener(CQEvents.STOP, () => {
|
||||||
|
APP.UI.hideStats();
|
||||||
|
room.removeCommand(Commands.CONNECTION_QUALITY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// listen to remote stats
|
||||||
|
room.addCommandListener(Commands.CONNECTION_QUALITY,(values, from) => {
|
||||||
|
APP.connectionquality.updateRemoteStats(from, values);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.connectionquality.addListener(CQEvents.REMOTESTATS_UPDATED,
|
||||||
|
(id, percent, stats) => {
|
||||||
|
APP.UI.updateRemoteStats(id, percent, stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.addCommandListener(Commands.ETHERPAD, ({value}) => {
|
||||||
|
APP.UI.initEtherpad(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.addCommandListener(Commands.PREZI, ({value, attributes}) => {
|
||||||
|
APP.UI.showPrezi(attributes.id, value, attributes.slide);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.addCommandListener(Commands.STOP_PREZI, ({attributes}) => {
|
||||||
|
APP.UI.stopPrezi(attributes.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.SHARE_PREZI, (url, slide) => {
|
||||||
|
console.log('Sharing Prezi %s slide %s', url, slide);
|
||||||
|
room.removeCommand(Commands.PREZI);
|
||||||
|
room.sendCommand(Commands.PREZI, {
|
||||||
|
value: url,
|
||||||
|
attributes: {
|
||||||
|
id: room.myUserId(),
|
||||||
|
slide
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.STOP_SHARING_PREZI, () => {
|
||||||
|
room.removeCommand(Commands.PREZI);
|
||||||
|
room.sendCommandOnce(Commands.STOP_PREZI, {
|
||||||
|
attributes: {
|
||||||
|
id: room.myUserId()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
room.addCommandListener(Commands.VIDEO_TYPE, ({value}, from) => {
|
||||||
|
APP.UI.onPeerVideoTypeChanged(from, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.EMAIL_CHANGED, (email) => {
|
||||||
|
APP.settings.setEmail(email);
|
||||||
|
APP.UI.setUserAvatar(room.myUserId(), email);
|
||||||
|
sendEmail(email);
|
||||||
|
});
|
||||||
|
room.addCommandListener(Commands.EMAIL, (data) => {
|
||||||
|
APP.UI.setUserAvatar(data.attributes.id, data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.NICKNAME_CHANGED, (nickname) => {
|
||||||
|
APP.settings.setDisplayName(nickname);
|
||||||
|
room.setDisplayName(nickname);
|
||||||
|
APP.UI.changeDisplayName(APP.conference.localId, nickname);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.START_MUTED_CHANGED,
|
||||||
|
(startAudioMuted, startVideoMuted) => {
|
||||||
|
room.setStartMutedPolicy({audio: startAudioMuted,
|
||||||
|
video: startVideoMuted});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
room.on(
|
||||||
|
ConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||||
|
(policy) => {
|
||||||
|
APP.UI.onStartMutedChanged();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
room.on(ConferenceEvents.STARTED_MUTED, () => {
|
||||||
|
(room.isStartAudioMuted() || room.isStartVideoMuted())
|
||||||
|
&& APP.UI.notifyInitiallyMuted();
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
|
||||||
|
APP.UI.inviteParticipants(
|
||||||
|
roomUrl,
|
||||||
|
APP.conference.roomName,
|
||||||
|
roomLocker.password,
|
||||||
|
APP.settings.getDisplayName()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
room.on(
|
||||||
|
ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
|
||||||
|
APP.UI.updateDevicesAvailability(id, devices);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// call hangup
|
||||||
|
APP.UI.addListener(UIEvents.HANGUP, () => {
|
||||||
|
APP.UI.requestFeedback().then(() => {
|
||||||
|
connection.disconnect();
|
||||||
|
config.enableWelcomePage && setTimeout(() => {
|
||||||
|
window.localStorage.welcomePageDisabled = false;
|
||||||
|
window.location.pathname = "/";
|
||||||
|
}, 3000);
|
||||||
|
}, (err) => {console.error(err);});
|
||||||
|
});
|
||||||
|
|
||||||
|
// logout
|
||||||
|
APP.UI.addListener(UIEvents.LOGOUT, () => {
|
||||||
|
// FIXME handle logout
|
||||||
|
// APP.xmpp.logout(function (url) {
|
||||||
|
// if (url) {
|
||||||
|
// window.location.href = url;
|
||||||
|
// } else {
|
||||||
|
// hangup();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.SIP_DIAL, (sipNumber) => {
|
||||||
|
room.dial(sipNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Starts or stops the recording for the conference.
|
||||||
|
APP.UI.addListener(UIEvents.RECORDING_TOGGLE, (predefinedToken) => {
|
||||||
|
if (predefinedToken) {
|
||||||
|
room.toggleRecording({token: predefinedToken});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
APP.UI.requestRecordingToken().then((token) => {
|
||||||
|
room.toggleRecording({token: token});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.SUBJECT_CHANGED, (topic) => {
|
||||||
|
room.setSubject(topic);
|
||||||
|
});
|
||||||
|
room.on(ConferenceEvents.SUBJECT_CHANGED, function (subject) {
|
||||||
|
APP.UI.setSubject(subject);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.USER_KICKED, (id) => {
|
||||||
|
room.kickParticipant(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.REMOTE_AUDIO_MUTED, (id) => {
|
||||||
|
room.muteParticipant(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
|
||||||
|
AuthHandler.authenticate(room);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
|
||||||
|
room.selectParticipant(id);
|
||||||
|
});
|
||||||
|
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
|
||||||
|
room.pinParticipant(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.UI.addListener(UIEvents.TOGGLE_SCREENSHARING, () => {
|
||||||
|
APP.desktopsharing.toggleScreenSharing();
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.desktopsharing.addListener(DSEvents.SWITCHING_DONE,
|
||||||
|
(isSharingScreen) => {
|
||||||
|
APP.UI.updateDesktopSharingButtons(isSharingScreen);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.desktopsharing.addListener(DSEvents.FIREFOX_EXTENSION_NEEDED,
|
||||||
|
(url) => {
|
||||||
|
APP.UI.showExtensionRequiredDialog(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
APP.desktopsharing.addListener(DSEvents.NEW_STREAM_CREATED,
|
||||||
|
(track, callback) => {
|
||||||
|
const localCallback = (newTrack) => {
|
||||||
|
if(!newTrack || !newTrack.isLocal() ||
|
||||||
|
newTrack !== localVideo)
|
||||||
|
return;
|
||||||
|
if(localVideo.isMuted() &&
|
||||||
|
localVideo.videoType !== track.videoType) {
|
||||||
|
localVideo.mute();
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
if(room)
|
||||||
|
room.off(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||||
|
};
|
||||||
|
if(room) {
|
||||||
|
room.on(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||||
|
}
|
||||||
|
localVideo.stop();
|
||||||
|
localVideo = track;
|
||||||
|
addTrack(track);
|
||||||
|
if(!room)
|
||||||
|
localCallback();
|
||||||
|
APP.UI.addLocalStream(track);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,107 @@
|
||||||
|
/* global APP, JitsiMeetJS, config */
|
||||||
|
//FIXME:
|
||||||
|
import LoginDialog from './modules/UI/authentication/LoginDialog';
|
||||||
|
|
||||||
|
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||||
|
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to open connection using provided credentials.
|
||||||
|
* @param {string} [id]
|
||||||
|
* @param {string} [password]
|
||||||
|
* @returns {Promise<JitsiConnection>} connection if
|
||||||
|
* everything is ok, else error.
|
||||||
|
*/
|
||||||
|
function connect(id, password) {
|
||||||
|
let connection = new JitsiMeetJS.JitsiConnection(null, null, config);
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
connection.addEventListener(
|
||||||
|
ConnectionEvents.CONNECTION_ESTABLISHED, handleConnectionEstablished
|
||||||
|
);
|
||||||
|
connection.addEventListener(
|
||||||
|
ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed
|
||||||
|
);
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
connection.removeEventListener(
|
||||||
|
ConnectionEvents.CONNECTION_ESTABLISHED,
|
||||||
|
handleConnectionEstablished
|
||||||
|
);
|
||||||
|
connection.removeEventListener(
|
||||||
|
ConnectionEvents.CONNECTION_FAILED,
|
||||||
|
handleConnectionFailed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConnectionEstablished() {
|
||||||
|
unsubscribe();
|
||||||
|
resolve(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConnectionFailed(err) {
|
||||||
|
unsubscribe();
|
||||||
|
console.error("CONNECTION FAILED:", err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.connect({id, password});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Authentication Dialog and try to connect with new credentials.
|
||||||
|
* If failed to connect because of PASSWORD_REQUIRED error
|
||||||
|
* then ask for password again.
|
||||||
|
* @returns {Promise<JitsiConnection>}
|
||||||
|
*/
|
||||||
|
function requestAuth() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
let authDialog = LoginDialog.showAuthDialog(
|
||||||
|
function (id, password) {
|
||||||
|
connect(id, password).then(function (connection) {
|
||||||
|
authDialog.close();
|
||||||
|
resolve(connection);
|
||||||
|
}, function (err) {
|
||||||
|
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||||
|
authDialog.displayError(err);
|
||||||
|
} else {
|
||||||
|
authDialog.close();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open JitsiConnection using provided credentials.
|
||||||
|
* If retry option is true it will show auth dialog on PASSWORD_REQUIRED error.
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} [options.id]
|
||||||
|
* @param {string} [options.password]
|
||||||
|
* @param {boolean} [retry] if we should show auth dialog
|
||||||
|
* on PASSWORD_REQUIRED error.
|
||||||
|
*
|
||||||
|
* @returns {Promise<JitsiConnection>}
|
||||||
|
*/
|
||||||
|
export function openConnection({id, password, retry}) {
|
||||||
|
return connect(id, password).catch(function (err) {
|
||||||
|
if (!retry) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||||
|
// do not retry if token is not valid
|
||||||
|
if (config.token) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
return requestAuth();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#remoteVideos .videocontainer {
|
#remoteVideos .videocontainer {
|
||||||
display: inline-block;
|
display: none;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
border-radius:8px;
|
border-radius:8px;
|
||||||
|
@ -407,7 +407,7 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#activeSpeaker {
|
#dominantSpeaker {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
|
@ -416,7 +416,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#activeSpeakerAudioLevel {
|
#dominantSpeakerAudioLevel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
@ -428,7 +428,7 @@
|
||||||
display:none !important;
|
display:none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#activeSpeakerAvatar {
|
#dominantSpeakerAvatar {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
top: 25px;
|
top: 25px;
|
||||||
|
|
|
@ -35,6 +35,6 @@ Description: Prosody configuration for Jitsi Meet
|
||||||
|
|
||||||
Package: jitsi-meet-tokens
|
Package: jitsi-meet-tokens
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly603), libssl-dev, luarocks, jitsi-meet-prosody
|
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly607), libssl-dev, luarocks, jitsi-meet-prosody
|
||||||
Description: Prosody token authentication plugin for Jitsi Meet
|
Description: Prosody token authentication plugin for Jitsi Meet
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,11 @@ case "$1" in
|
||||||
if [ -x "/etc/init.d/prosody" ]; then
|
if [ -x "/etc/init.d/prosody" ]; then
|
||||||
invoke-rc.d prosody restart
|
invoke-rc.d prosody restart
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "This package requires BOSH Prosody module to be patched !"
|
||||||
|
echo "Use the following command, after this package has been installed and"
|
||||||
|
echo "after every prosody-trunk upgrade:"
|
||||||
|
echo "sudo patch -N /usr/lib/prosody/modules/mod_bosh.lua /usr/share/jitsi-meet/prosody-plugins/mod_bosh.lua.patch"
|
||||||
else
|
else
|
||||||
echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet"
|
echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet"
|
||||||
fi
|
fi
|
||||||
|
|
24
index.html
24
index.html
|
@ -13,6 +13,7 @@
|
||||||
<script>console.log("(TIME) index.html loaded:\t", window.performance.now());</script>
|
<script>console.log("(TIME) index.html loaded:\t", window.performance.now());</script>
|
||||||
<script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
<script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||||
<script src="interface_config.js?v=6"></script>
|
<script src="interface_config.js?v=6"></script>
|
||||||
|
<script src="libs/lib-jitsi-meet.js?v=139"></script>
|
||||||
<script src="libs/app.bundle.min.js?v=139"></script>
|
<script src="libs/app.bundle.min.js?v=139"></script>
|
||||||
<!--
|
<!--
|
||||||
Link used for inline installation of chrome desktop streaming extension,
|
Link used for inline installation of chrome desktop streaming extension,
|
||||||
|
@ -139,13 +140,32 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||||
<div id="videospace">
|
<div id="videospace">
|
||||||
|
|
||||||
|
<div id="largeVideoContainer" class="videocontainer">
|
||||||
|
<div id="presentation"></div>
|
||||||
|
<div id="etherpad"></div>
|
||||||
|
<a target="_new"><div class="watermark leftwatermark"></div></a>
|
||||||
|
<a target="_new"><div class="watermark rightwatermark"></div></a>
|
||||||
|
<a class="poweredby" href="http://jitsi.org" target="_new">
|
||||||
|
<span data-i18n="poweredby"></span> jitsi.org
|
||||||
|
</a>
|
||||||
|
<div id="dominantSpeaker">
|
||||||
|
<img id="dominantSpeakerAvatar" src=""/>
|
||||||
|
<canvas id="dominantSpeakerAudioLevel"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="largeVideoWrapper">
|
||||||
|
<video id="largeVideo" muted="true" autoplay></video>
|
||||||
|
</div>
|
||||||
|
<span id="videoConnectionMessage"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="remoteVideos">
|
<div id="remoteVideos">
|
||||||
<span id="localVideoContainer" class="videocontainer">
|
<span id="localVideoContainer" class="videocontainer">
|
||||||
<span id="localNick" class="nick"></span>
|
<span id="localNick" class="nick"></span>
|
||||||
<span id="localVideoWrapper">
|
<span id="localVideoWrapper">
|
||||||
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
|
<!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
|
||||||
</span>
|
</span>
|
||||||
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
<audio id="localAudio" autoplay muted></audio>
|
||||||
<span class="focusindicator"></span>
|
<span class="focusindicator"></span>
|
||||||
</span>
|
</span>
|
||||||
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
||||||
|
|
|
@ -14,7 +14,7 @@ var interfaceConfig = {
|
||||||
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
||||||
APP_NAME: "Jitsi Meet",
|
APP_NAME: "Jitsi Meet",
|
||||||
INVITATION_POWERED_BY: true,
|
INVITATION_POWERED_BY: true,
|
||||||
ACTIVE_SPEAKER_AVATAR_SIZE: 100,
|
DOMINANT_SPEAKER_AVATAR_SIZE: 100,
|
||||||
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
|
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
|
||||||
'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad',
|
'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad',
|
||||||
'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',
|
'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,8 +5,6 @@
|
||||||
* applications that embed Jitsi Meet
|
* applications that embed Jitsi Meet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of the available commands.
|
* List of the available commands.
|
||||||
* @type {{
|
* @type {{
|
||||||
|
@ -23,8 +21,8 @@ var commands = {};
|
||||||
function initCommands() {
|
function initCommands() {
|
||||||
commands = {
|
commands = {
|
||||||
displayName: APP.UI.inputDisplayNameHandler,
|
displayName: APP.UI.inputDisplayNameHandler,
|
||||||
toggleAudio: APP.UI.toggleAudio,
|
toggleAudio: APP.conference.toggleAudioMuted,
|
||||||
toggleVideo: APP.UI.toggleVideo,
|
toggleVideo: APP.conference.toggleVideoMuted,
|
||||||
toggleFilmStrip: APP.UI.toggleFilmStrip,
|
toggleFilmStrip: APP.UI.toggleFilmStrip,
|
||||||
toggleChat: APP.UI.toggleChat,
|
toggleChat: APP.UI.toggleChat,
|
||||||
toggleContactList: APP.UI.toggleContactList
|
toggleContactList: APP.UI.toggleContactList
|
||||||
|
@ -43,7 +41,7 @@ function initCommands() {
|
||||||
* participantLeft: boolean
|
* participantLeft: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
var events = {
|
const events = {
|
||||||
incomingMessage: false,
|
incomingMessage: false,
|
||||||
outgoingMessage:false,
|
outgoingMessage:false,
|
||||||
displayNameChange: false,
|
displayNameChange: false,
|
||||||
|
@ -51,8 +49,6 @@ var events = {
|
||||||
participantLeft: false
|
participantLeft: false
|
||||||
};
|
};
|
||||||
|
|
||||||
var displayName = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes commands from external application.
|
* Processes commands from external application.
|
||||||
* @param message the object with the command
|
* @param message the object with the command
|
||||||
|
@ -128,44 +124,42 @@ function processMessage(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupListeners() {
|
/**
|
||||||
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, function (from) {
|
|
||||||
API.triggerEvent("participantJoined", {jid: from});
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED,
|
|
||||||
function (from, nick, txt, myjid, stamp) {
|
|
||||||
if (from != myjid)
|
|
||||||
API.triggerEvent("incomingMessage",
|
|
||||||
{"from": from, "nick": nick, "message": txt, "stamp": stamp});
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, function (jid) {
|
|
||||||
API.triggerEvent("participantLeft", {jid: jid});
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
|
|
||||||
function (jid, newDisplayName) {
|
|
||||||
var name = displayName[jid];
|
|
||||||
if(!name || name != newDisplayName) {
|
|
||||||
API.triggerEvent("displayNameChange",
|
|
||||||
{jid: jid, displayname: newDisplayName});
|
|
||||||
displayName[jid] = newDisplayName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.SENDING_CHAT_MESSAGE, function (body) {
|
|
||||||
APP.API.triggerEvent("outgoingMessage", {"message": body});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var API = {
|
|
||||||
/**
|
|
||||||
* Check whether the API should be enabled or not.
|
* Check whether the API should be enabled or not.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isEnabled: function () {
|
function isEnabled () {
|
||||||
var hash = location.hash;
|
let hash = location.hash;
|
||||||
if (hash && hash.indexOf("external") > -1 && window.postMessage)
|
return hash && hash.indexOf("external") > -1 && window.postMessage;
|
||||||
return true;
|
}
|
||||||
return false;
|
|
||||||
},
|
/**
|
||||||
|
* Checks whether the event is enabled ot not.
|
||||||
|
* @param name the name of the event.
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function isEventEnabled (name) {
|
||||||
|
return events[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends event object to the external application that has been subscribed
|
||||||
|
* for that event.
|
||||||
|
* @param name the name event
|
||||||
|
* @param object data associated with the event
|
||||||
|
*/
|
||||||
|
function triggerEvent (name, object) {
|
||||||
|
if (isEnabled() && isEventEnabled(name)) {
|
||||||
|
sendMessage({
|
||||||
|
type: "event",
|
||||||
|
action: "result",
|
||||||
|
event: name,
|
||||||
|
result: object
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
/**
|
/**
|
||||||
* Initializes the APIConnector. Setups message event listeners that will
|
* Initializes the APIConnector. Setups message event listeners that will
|
||||||
* receive information from external applications that embed Jitsi Meet.
|
* receive information from external applications that embed Jitsi Meet.
|
||||||
|
@ -173,50 +167,85 @@ var API = {
|
||||||
* is initialized.
|
* is initialized.
|
||||||
*/
|
*/
|
||||||
init: function () {
|
init: function () {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
initCommands();
|
initCommands();
|
||||||
if (window.addEventListener) {
|
if (window.addEventListener) {
|
||||||
window.addEventListener('message',
|
window.addEventListener('message', processMessage, false);
|
||||||
processMessage, false);
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.attachEvent('onmessage', processMessage);
|
window.attachEvent('onmessage', processMessage);
|
||||||
}
|
}
|
||||||
sendMessage({type: "system", loaded: true});
|
sendMessage({type: "system", loaded: true});
|
||||||
setupListeners();
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Checks whether the event is enabled ot not.
|
|
||||||
* @param name the name of the event.
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
isEventEnabled: function (name) {
|
|
||||||
return events[name];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends event object to the external application that has been subscribed
|
* Notify external application (if API is enabled) that message was sent.
|
||||||
* for that event.
|
* @param {string} body message body
|
||||||
* @param name the name event
|
|
||||||
* @param object data associated with the event
|
|
||||||
*/
|
*/
|
||||||
triggerEvent: function (name, object) {
|
notifySendingChatMessage (body) {
|
||||||
if(this.isEnabled() && this.isEventEnabled(name))
|
triggerEvent("outgoingMessage", {"message": body});
|
||||||
sendMessage({
|
},
|
||||||
type: "event", action: "result", event: name, result: object});
|
|
||||||
|
/**
|
||||||
|
* Notify external application (if API is enabled) that
|
||||||
|
* message was received.
|
||||||
|
* @param {string} id user id
|
||||||
|
* @param {string} nick user nickname
|
||||||
|
* @param {string} body message body
|
||||||
|
* @param {number} ts message creation timestamp
|
||||||
|
*/
|
||||||
|
notifyReceivedChatMessage (id, nick, body, ts) {
|
||||||
|
if (APP.conference.isLocalId(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvent(
|
||||||
|
"incomingMessage",
|
||||||
|
{"from": id, "nick": nick, "message": body, "stamp": ts}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify external application (if API is enabled) that
|
||||||
|
* user joined the conference.
|
||||||
|
* @param {string} id user id
|
||||||
|
*/
|
||||||
|
notifyUserJoined (id) {
|
||||||
|
triggerEvent("participantJoined", {id});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify external application (if API is enabled) that
|
||||||
|
* user left the conference.
|
||||||
|
* @param {string} id user id
|
||||||
|
*/
|
||||||
|
notifyUserLeft (id) {
|
||||||
|
triggerEvent("participantLeft", {id});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify external application (if API is enabled) that
|
||||||
|
* user changed their nickname.
|
||||||
|
* @param {string} id user id
|
||||||
|
* @param {string} displayName user nickname
|
||||||
|
*/
|
||||||
|
notifyDisplayNameChanged (id, displayName) {
|
||||||
|
triggerEvent("displayNameChange", {id, displayname: displayName});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the listeners.
|
* Removes the listeners.
|
||||||
*/
|
*/
|
||||||
dispose: function () {
|
dispose: function () {
|
||||||
if(window.removeEventListener) {
|
if (!isEnabled()) {
|
||||||
window.removeEventListener("message",
|
return;
|
||||||
processMessage, false);
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
|
if (window.removeEventListener) {
|
||||||
|
window.removeEventListener("message", processMessage, false);
|
||||||
|
} else {
|
||||||
window.detachEvent('onmessage', processMessage);
|
window.detachEvent('onmessage', processMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = API;
|
|
|
@ -1,47 +0,0 @@
|
||||||
/* global APP */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A module for sending DTMF tones.
|
|
||||||
*/
|
|
||||||
var DTMFSender;
|
|
||||||
var initDtmfSender = function() {
|
|
||||||
// TODO: This needs to reset this if the peerconnection changes
|
|
||||||
// (e.g. the call is re-made)
|
|
||||||
if (DTMFSender)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var localAudio = APP.RTC.localAudio;
|
|
||||||
if (localAudio && localAudio.getTracks().length > 0)
|
|
||||||
{
|
|
||||||
var peerconnection
|
|
||||||
= APP.xmpp.getConnection().jingle.activecall.peerconnection;
|
|
||||||
if (peerconnection) {
|
|
||||||
DTMFSender =
|
|
||||||
peerconnection.peerconnection
|
|
||||||
.createDTMFSender(localAudio.getTracks()[0]);
|
|
||||||
console.log("Initialized DTMFSender");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("Failed to initialize DTMFSender: no PeerConnection.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("Failed to initialize DTMFSender: no audio track.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var DTMF = {
|
|
||||||
sendTones: function (tones, duration, pause) {
|
|
||||||
if (!DTMFSender)
|
|
||||||
initDtmfSender();
|
|
||||||
|
|
||||||
if (DTMFSender){
|
|
||||||
DTMFSender.insertDTMF(tones,
|
|
||||||
(duration || 200),
|
|
||||||
(pause || 200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = DTMF;
|
|
||||||
|
|
|
@ -1,211 +0,0 @@
|
||||||
/* global config, APP, Strophe */
|
|
||||||
/* jshint -W101 */
|
|
||||||
|
|
||||||
// cache datachannels to avoid garbage collection
|
|
||||||
// https://code.google.com/p/chromium/issues/detail?id=405545
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
|
|
||||||
var _dataChannels = [];
|
|
||||||
var eventEmitter = null;
|
|
||||||
|
|
||||||
var DataChannels = {
|
|
||||||
/**
|
|
||||||
* Callback triggered by PeerConnection when new data channel is opened
|
|
||||||
* on the bridge.
|
|
||||||
* @param event the event info object.
|
|
||||||
*/
|
|
||||||
onDataChannel: function (event) {
|
|
||||||
var dataChannel = event.channel;
|
|
||||||
|
|
||||||
dataChannel.onopen = function () {
|
|
||||||
console.info("Data channel opened by the Videobridge!", dataChannel);
|
|
||||||
|
|
||||||
// Code sample for sending string and/or binary data
|
|
||||||
// Sends String message to the bridge
|
|
||||||
//dataChannel.send("Hello bridge!");
|
|
||||||
// Sends 12 bytes binary message to the bridge
|
|
||||||
//dataChannel.send(new ArrayBuffer(12));
|
|
||||||
|
|
||||||
eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onerror = function (error) {
|
|
||||||
console.error("Data Channel Error:", error, dataChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onmessage = function (event) {
|
|
||||||
var data = event.data;
|
|
||||||
// JSON
|
|
||||||
var obj;
|
|
||||||
|
|
||||||
try {
|
|
||||||
obj = JSON.parse(data);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(
|
|
||||||
"Failed to parse data channel message as JSON: ",
|
|
||||||
data,
|
|
||||||
dataChannel);
|
|
||||||
}
|
|
||||||
if (('undefined' !== typeof(obj)) && (null !== obj)) {
|
|
||||||
var colibriClass = obj.colibriClass;
|
|
||||||
|
|
||||||
if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
|
|
||||||
// Endpoint ID from the Videobridge.
|
|
||||||
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Data channel new dominant speaker event: ",
|
|
||||||
dominantSpeakerEndpoint);
|
|
||||||
eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
|
|
||||||
}
|
|
||||||
else if ("InLastNChangeEvent" === colibriClass) {
|
|
||||||
var oldValue = obj.oldValue;
|
|
||||||
var newValue = obj.newValue;
|
|
||||||
// Make sure that oldValue and newValue are of type boolean.
|
|
||||||
var type;
|
|
||||||
|
|
||||||
if ((type = typeof oldValue) !== 'boolean') {
|
|
||||||
if (type === 'string') {
|
|
||||||
oldValue = (oldValue == "true");
|
|
||||||
} else {
|
|
||||||
oldValue = Boolean(oldValue).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((type = typeof newValue) !== 'boolean') {
|
|
||||||
if (type === 'string') {
|
|
||||||
newValue = (newValue == "true");
|
|
||||||
} else {
|
|
||||||
newValue = Boolean(newValue).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
|
|
||||||
}
|
|
||||||
else if ("LastNEndpointsChangeEvent" === colibriClass) {
|
|
||||||
// The new/latest list of last-n endpoint IDs.
|
|
||||||
var lastNEndpoints = obj.lastNEndpoints;
|
|
||||||
// The list of endpoint IDs which are entering the list of
|
|
||||||
// last-n at this time i.e. were not in the old list of last-n
|
|
||||||
// endpoint IDs.
|
|
||||||
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Data channel new last-n event: ",
|
|
||||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
|
||||||
eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
|
|
||||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.debug("Data channel JSON-formatted message: ", obj);
|
|
||||||
// The received message appears to be appropriately
|
|
||||||
// formatted (i.e. is a JSON object which assigns a value to
|
|
||||||
// the mandatory property colibriClass) so don't just
|
|
||||||
// swallow it, expose it to public consumption.
|
|
||||||
eventEmitter.emit("rtc.datachannel." + colibriClass, obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onclose = function () {
|
|
||||||
console.info("The Data Channel closed", dataChannel);
|
|
||||||
var idx = _dataChannels.indexOf(dataChannel);
|
|
||||||
if (idx > -1)
|
|
||||||
_dataChannels = _dataChannels.splice(idx, 1);
|
|
||||||
};
|
|
||||||
_dataChannels.push(dataChannel);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds "ondatachannel" event listener to given PeerConnection instance.
|
|
||||||
* @param peerConnection WebRTC peer connection instance.
|
|
||||||
*/
|
|
||||||
init: function (peerConnection, emitter) {
|
|
||||||
if(!config.openSctp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
peerConnection.ondatachannel = this.onDataChannel;
|
|
||||||
eventEmitter = emitter;
|
|
||||||
|
|
||||||
// Sample code for opening new data channel from Jitsi Meet to the bridge.
|
|
||||||
// Although it's not a requirement to open separate channels from both bridge
|
|
||||||
// and peer as single channel can be used for sending and receiving data.
|
|
||||||
// So either channel opened by the bridge or the one opened here is enough
|
|
||||||
// for communication with the bridge.
|
|
||||||
/*var dataChannelOptions =
|
|
||||||
{
|
|
||||||
reliable: true
|
|
||||||
};
|
|
||||||
var dataChannel
|
|
||||||
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
|
|
||||||
|
|
||||||
// Can be used only when is in open state
|
|
||||||
dataChannel.onopen = function ()
|
|
||||||
{
|
|
||||||
dataChannel.send("My channel !!!");
|
|
||||||
};
|
|
||||||
dataChannel.onmessage = function (event)
|
|
||||||
{
|
|
||||||
var msgData = event.data;
|
|
||||||
console.info("Got My Data Channel Message:", msgData, dataChannel);
|
|
||||||
};*/
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSelectedEndpointEvent: function (userResource) {
|
|
||||||
onXXXEndpointChanged("selected", userResource);
|
|
||||||
},
|
|
||||||
handlePinnedEndpointEvent: function (userResource) {
|
|
||||||
onXXXEndpointChanged("pinned", userResource);
|
|
||||||
},
|
|
||||||
|
|
||||||
some: function (callback, thisArg) {
|
|
||||||
if (_dataChannels && _dataChannels.length !== 0) {
|
|
||||||
if (thisArg)
|
|
||||||
return _dataChannels.some(callback, thisArg);
|
|
||||||
else
|
|
||||||
return _dataChannels.some(callback);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies Videobridge about a change in the value of a specific
|
|
||||||
* endpoint-related property such as selected endpoint and pinnned endpoint.
|
|
||||||
*
|
|
||||||
* @param xxx the name of the endpoint-related property whose value changed
|
|
||||||
* @param userResource the new value of the endpoint-related property after the
|
|
||||||
* change
|
|
||||||
*/
|
|
||||||
function onXXXEndpointChanged(xxx, userResource) {
|
|
||||||
// Derive the correct words from xxx such as selected and Selected, pinned
|
|
||||||
// and Pinned.
|
|
||||||
var head = xxx.charAt(0);
|
|
||||||
var tail = xxx.substring(1);
|
|
||||||
var lower = head.toLowerCase() + tail;
|
|
||||||
var upper = head.toUpperCase() + tail;
|
|
||||||
|
|
||||||
// Notify Videobridge about the specified endpoint change.
|
|
||||||
console.log(lower + ' endpoint changed: ', userResource);
|
|
||||||
DataChannels.some(function (dataChannel) {
|
|
||||||
if (dataChannel.readyState == 'open') {
|
|
||||||
console.log(
|
|
||||||
'sending ' + lower
|
|
||||||
+ ' endpoint changed notification to the bridge: ',
|
|
||||||
userResource);
|
|
||||||
|
|
||||||
var jsonObject = {};
|
|
||||||
|
|
||||||
jsonObject.colibriClass = (upper + 'EndpointChangedEvent');
|
|
||||||
jsonObject[lower + "Endpoint"]
|
|
||||||
= (userResource ? userResource : null);
|
|
||||||
dataChannel.send(JSON.stringify(jsonObject));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DataChannels;
|
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
/* global APP */
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This implements 'onended' callback normally fired by WebRTC after the stream
|
|
||||||
* is stopped. There is no such behaviour yet in FF, so we have to add it.
|
|
||||||
* @param stream original WebRTC stream object to which 'onended' handling
|
|
||||||
* will be added.
|
|
||||||
*/
|
|
||||||
function implementOnEndedHandling(localStream) {
|
|
||||||
var stream = localStream.getOriginalStream();
|
|
||||||
var originalStop = stream.stop;
|
|
||||||
stream.stop = function () {
|
|
||||||
originalStop.apply(stream);
|
|
||||||
if (localStream.isActive()) {
|
|
||||||
stream.onended();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) {
|
|
||||||
this.stream = stream;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
this.type = type;
|
|
||||||
this.videoType = videoType;
|
|
||||||
this.isGUMStream = true;
|
|
||||||
if(isGUMStream === false)
|
|
||||||
this.isGUMStream = isGUMStream;
|
|
||||||
var self = this;
|
|
||||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
|
||||||
this.getTracks = function () {
|
|
||||||
return self.stream.getAudioTracks();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.getTracks = function () {
|
|
||||||
return self.stream.getVideoTracks();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
APP.RTC.addMediaStreamInactiveHandler(
|
|
||||||
this.stream,
|
|
||||||
function () {
|
|
||||||
self.streamEnded();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
implementOnEndedHandling(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStream.prototype.streamEnded = function () {
|
|
||||||
this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.getOriginalStream = function()
|
|
||||||
{
|
|
||||||
return this.stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isAudioStream = function () {
|
|
||||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isVideoStream = function () {
|
|
||||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.setMute = function (mute)
|
|
||||||
{
|
|
||||||
var isAudio = this.isAudioStream();
|
|
||||||
var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE;
|
|
||||||
|
|
||||||
if ((window.location.protocol != "https:" && this.isGUMStream) ||
|
|
||||||
(isAudio && this.isGUMStream) || this.videoType === "screen" ||
|
|
||||||
// FIXME FF does not support 'removeStream' method used to mute
|
|
||||||
RTCBrowserType.isFirefox()) {
|
|
||||||
|
|
||||||
var tracks = this.getTracks();
|
|
||||||
for (var idx = 0; idx < tracks.length; idx++) {
|
|
||||||
tracks[idx].enabled = !mute;
|
|
||||||
}
|
|
||||||
this.eventEmitter.emit(eventType, mute);
|
|
||||||
} else {
|
|
||||||
if (mute) {
|
|
||||||
APP.xmpp.removeStream(this.stream);
|
|
||||||
APP.RTC.stopMediaStream(this.stream);
|
|
||||||
this.eventEmitter.emit(eventType, true);
|
|
||||||
} else {
|
|
||||||
var self = this;
|
|
||||||
APP.RTC.rtcUtils.obtainAudioAndVideoPermissions(
|
|
||||||
(this.isAudioStream() ? ["audio"] : ["video"]),
|
|
||||||
function (stream) {
|
|
||||||
if (isAudio) {
|
|
||||||
APP.RTC.changeLocalAudio(stream,
|
|
||||||
function () {
|
|
||||||
self.eventEmitter.emit(eventType, false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
APP.RTC.changeLocalVideo(stream, false,
|
|
||||||
function () {
|
|
||||||
self.eventEmitter.emit(eventType, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isMuted = function () {
|
|
||||||
var tracks = [];
|
|
||||||
if (this.isAudioStream()) {
|
|
||||||
tracks = this.stream.getAudioTracks();
|
|
||||||
} else {
|
|
||||||
if (!this.isActive())
|
|
||||||
return true;
|
|
||||||
tracks = this.stream.getVideoTracks();
|
|
||||||
}
|
|
||||||
for (var idx = 0; idx < tracks.length; idx++) {
|
|
||||||
if(tracks[idx].enabled)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.getId = function () {
|
|
||||||
return this.stream.getTracks()[0].id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the MediaStream is avtive/not ended.
|
|
||||||
* When there is no check for active we don't have information and so
|
|
||||||
* will return that stream is active (in case of FF).
|
|
||||||
* @returns {boolean} whether MediaStream is active.
|
|
||||||
*/
|
|
||||||
LocalStream.prototype.isActive = function () {
|
|
||||||
if((typeof this.stream.active !== "undefined"))
|
|
||||||
return this.stream.active;
|
|
||||||
else
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LocalStream;
|
|
|
@ -1,57 +0,0 @@
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a MediaStream object for the given data, session id and ssrc.
|
|
||||||
* It is a wrapper class for the MediaStream.
|
|
||||||
*
|
|
||||||
* @param data the data object from which we obtain the stream,
|
|
||||||
* the peerjid, etc.
|
|
||||||
* @param ssrc the ssrc corresponding to this MediaStream
|
|
||||||
* @param mute the whether this MediaStream is muted
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function MediaStream(data, ssrc, browser, eventEmitter, muted, type) {
|
|
||||||
|
|
||||||
// XXX(gp) to minimize headaches in the future, we should build our
|
|
||||||
// abstractions around tracks and not streams. ORTC is track based API.
|
|
||||||
// Mozilla expects m-lines to represent media tracks.
|
|
||||||
//
|
|
||||||
// Practically, what I'm saying is that we should have a MediaTrack class
|
|
||||||
// and not a MediaStream class.
|
|
||||||
//
|
|
||||||
// Also, we should be able to associate multiple SSRCs with a MediaTrack as
|
|
||||||
// a track might have an associated RTX and FEC sources.
|
|
||||||
|
|
||||||
if (!type) {
|
|
||||||
console.log("Errrm...some code needs an update...");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stream = data.stream;
|
|
||||||
this.peerjid = data.peerjid;
|
|
||||||
this.videoType = data.videoType;
|
|
||||||
this.ssrc = ssrc;
|
|
||||||
this.type = type;
|
|
||||||
this.muted = muted;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME duplicated with LocalStream methods - extract base class
|
|
||||||
MediaStream.prototype.isAudioStream = function () {
|
|
||||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.isVideoStream = function () {
|
|
||||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.getOriginalStream = function () {
|
|
||||||
return this.stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.setMute = function (value) {
|
|
||||||
this.stream.muted = value;
|
|
||||||
this.muted = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = MediaStream;
|
|
|
@ -1,335 +0,0 @@
|
||||||
/* global APP */
|
|
||||||
var EventEmitter = require("events");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var RTCUtils = require("./RTCUtils.js");
|
|
||||||
var LocalStream = require("./LocalStream.js");
|
|
||||||
var DataChannels = require("./DataChannels");
|
|
||||||
var MediaStream = require("./MediaStream.js");
|
|
||||||
var DesktopSharingEventTypes
|
|
||||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents.js");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var UIEvents = require("../../service/UI/UIEvents");
|
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
|
|
||||||
function getMediaStreamUsage()
|
|
||||||
{
|
|
||||||
var result = {
|
|
||||||
audio: true,
|
|
||||||
video: true
|
|
||||||
};
|
|
||||||
|
|
||||||
/** There are some issues with the desktop sharing
|
|
||||||
* when this property is enabled.
|
|
||||||
* WARNING: We must change the implementation to start video/audio if we
|
|
||||||
* receive from the focus that the peer is not muted.
|
|
||||||
|
|
||||||
var isSecureConnection = window.location.protocol == "https:";
|
|
||||||
|
|
||||||
if(config.disableEarlyMediaPermissionRequests || !isSecureConnection)
|
|
||||||
{
|
|
||||||
result = {
|
|
||||||
audio: false,
|
|
||||||
video: false
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
**/
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var RTC = {
|
|
||||||
// Exposes DataChannels to public consumption (e.g. jitsi-meet-torture)
|
|
||||||
// without the necessity to require the module.
|
|
||||||
"DataChannels": DataChannels,
|
|
||||||
|
|
||||||
rtcUtils: null,
|
|
||||||
devices: {
|
|
||||||
audio: true,
|
|
||||||
video: true
|
|
||||||
},
|
|
||||||
remoteStreams: {},
|
|
||||||
localAudio: null,
|
|
||||||
localVideo: null,
|
|
||||||
addStreamListener: function (listener, eventType) {
|
|
||||||
eventEmitter.on(eventType, listener);
|
|
||||||
},
|
|
||||||
addListener: function (type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
},
|
|
||||||
removeStreamListener: function (listener, eventType) {
|
|
||||||
if(!(eventType instanceof StreamEventTypes))
|
|
||||||
throw "Illegal argument";
|
|
||||||
|
|
||||||
eventEmitter.removeListener(eventType, listener);
|
|
||||||
},
|
|
||||||
createLocalStream: function (stream, type, change, videoType,
|
|
||||||
isMuted, isGUMStream) {
|
|
||||||
|
|
||||||
var localStream =
|
|
||||||
new LocalStream(stream, type, eventEmitter, videoType, isGUMStream);
|
|
||||||
if(isMuted === true)
|
|
||||||
localStream.setMute(true);
|
|
||||||
|
|
||||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
|
||||||
this.localAudio = localStream;
|
|
||||||
} else {
|
|
||||||
this.localVideo = localStream;
|
|
||||||
}
|
|
||||||
var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED;
|
|
||||||
if(change)
|
|
||||||
eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED;
|
|
||||||
|
|
||||||
eventEmitter.emit(eventType, localStream, isMuted);
|
|
||||||
return localStream;
|
|
||||||
},
|
|
||||||
createRemoteStream: function (data, ssrc) {
|
|
||||||
var jid = data.peerjid || APP.xmpp.myJid();
|
|
||||||
|
|
||||||
// check the video muted state from last stored presence if any
|
|
||||||
var muted = false;
|
|
||||||
var pres = APP.xmpp.getLastPresence(jid);
|
|
||||||
if (pres && pres.videoMuted) {
|
|
||||||
muted = pres.videoMuted;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
[MediaStreamType.AUDIO_TYPE, MediaStreamType.VIDEO_TYPE].forEach(
|
|
||||||
function (type) {
|
|
||||||
var tracks =
|
|
||||||
type == MediaStreamType.AUDIO_TYPE
|
|
||||||
? data.stream.getAudioTracks() : data.stream.getVideoTracks();
|
|
||||||
if (!tracks || !Array.isArray(tracks) || !tracks.length) {
|
|
||||||
console.log("Not creating a(n) " + type + " stream: no tracks");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var remoteStream = new MediaStream(data, ssrc,
|
|
||||||
RTCBrowserType.getBrowserType(), eventEmitter, muted, type);
|
|
||||||
|
|
||||||
if (!self.remoteStreams[jid]) {
|
|
||||||
self.remoteStreams[jid] = {};
|
|
||||||
}
|
|
||||||
self.remoteStreams[jid][type] = remoteStream;
|
|
||||||
eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED,
|
|
||||||
remoteStream);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getPCConstraints: function () {
|
|
||||||
return this.rtcUtils.pc_constraints;
|
|
||||||
},
|
|
||||||
getUserMediaWithConstraints:function(um, success_callback,
|
|
||||||
failure_callback, resolution,
|
|
||||||
bandwidth, fps, desktopStream)
|
|
||||||
{
|
|
||||||
return this.rtcUtils.getUserMediaWithConstraints(um, success_callback,
|
|
||||||
failure_callback, resolution, bandwidth, fps, desktopStream);
|
|
||||||
},
|
|
||||||
attachMediaStream: function (elSelector, stream) {
|
|
||||||
this.rtcUtils.attachMediaStream(elSelector, stream);
|
|
||||||
},
|
|
||||||
getStreamID: function (stream) {
|
|
||||||
return this.rtcUtils.getStreamID(stream);
|
|
||||||
},
|
|
||||||
getVideoSrc: function (element) {
|
|
||||||
return this.rtcUtils.getVideoSrc(element);
|
|
||||||
},
|
|
||||||
setVideoSrc: function (element, src) {
|
|
||||||
this.rtcUtils.setVideoSrc(element, src);
|
|
||||||
},
|
|
||||||
getVideoElementName: function () {
|
|
||||||
return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video';
|
|
||||||
},
|
|
||||||
dispose: function() {
|
|
||||||
if (this.rtcUtils) {
|
|
||||||
this.rtcUtils = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop: function () {
|
|
||||||
this.dispose();
|
|
||||||
},
|
|
||||||
start: function () {
|
|
||||||
var self = this;
|
|
||||||
APP.desktopsharing.addListener(
|
|
||||||
DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
|
||||||
function (stream, isUsingScreenStream, callback) {
|
|
||||||
self.changeLocalVideo(stream, isUsingScreenStream, callback);
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function(event) {
|
|
||||||
DataChannels.init(event.peerconnection, eventEmitter);
|
|
||||||
});
|
|
||||||
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT,
|
|
||||||
DataChannels.handleSelectedEndpointEvent);
|
|
||||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
|
||||||
DataChannels.handlePinnedEndpointEvent);
|
|
||||||
|
|
||||||
// In case of IE we continue from 'onReady' callback
|
|
||||||
// passed to RTCUtils constructor. It will be invoked by Temasys plugin
|
|
||||||
// once it is initialized.
|
|
||||||
var onReady = function () {
|
|
||||||
eventEmitter.emit(RTCEvents.RTC_READY, true);
|
|
||||||
self.rtcUtils.obtainAudioAndVideoPermissions(
|
|
||||||
null, null, getMediaStreamUsage());
|
|
||||||
};
|
|
||||||
|
|
||||||
this.rtcUtils = new RTCUtils(this, eventEmitter, onReady);
|
|
||||||
|
|
||||||
// Call onReady() if Temasys plugin is not used
|
|
||||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
onReady();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
muteRemoteVideoStream: function (jid, value) {
|
|
||||||
var stream;
|
|
||||||
|
|
||||||
if(this.remoteStreams[jid] &&
|
|
||||||
this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
|
||||||
stream = this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!stream)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (value != stream.muted) {
|
|
||||||
stream.setMute(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
changeLocalVideo: function (stream, isUsingScreenStream, callback) {
|
|
||||||
var oldStream = this.localVideo.getOriginalStream();
|
|
||||||
var type = (isUsingScreenStream ? "screen" : "camera");
|
|
||||||
var localCallback = callback;
|
|
||||||
if(this.localVideo.isMuted() && this.localVideo.videoType !== type) {
|
|
||||||
localCallback = function() {
|
|
||||||
APP.xmpp.setVideoMute(false, function(mute) {
|
|
||||||
eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
|
|
||||||
});
|
|
||||||
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// FIXME: Workaround for FF/IE/Safari
|
|
||||||
if (stream && stream.videoStream) {
|
|
||||||
stream = stream.videoStream;
|
|
||||||
}
|
|
||||||
var videoStream = this.rtcUtils.createStream(stream, true);
|
|
||||||
this.localVideo =
|
|
||||||
this.createLocalStream(videoStream, "video", true, type);
|
|
||||||
// Stop the stream
|
|
||||||
this.stopMediaStream(oldStream);
|
|
||||||
|
|
||||||
APP.xmpp.switchStreams(videoStream, oldStream,localCallback);
|
|
||||||
},
|
|
||||||
changeLocalAudio: function (stream, callback) {
|
|
||||||
var oldStream = this.localAudio.getOriginalStream();
|
|
||||||
var newStream = this.rtcUtils.createStream(stream);
|
|
||||||
this.localAudio
|
|
||||||
= this.createLocalStream(
|
|
||||||
newStream, MediaStreamType.AUDIO_TYPE, true);
|
|
||||||
// Stop the stream
|
|
||||||
this.stopMediaStream(oldStream);
|
|
||||||
APP.xmpp.switchStreams(newStream, oldStream, callback, true);
|
|
||||||
},
|
|
||||||
isVideoMuted: function (jid) {
|
|
||||||
if (jid === APP.xmpp.myJid()) {
|
|
||||||
var localVideo = APP.RTC.localVideo;
|
|
||||||
return (!localVideo || localVideo.isMuted());
|
|
||||||
} else {
|
|
||||||
if (!APP.RTC.remoteStreams[jid] ||
|
|
||||||
!APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setVideoMute: function (mute, callback, options) {
|
|
||||||
if (!this.localVideo)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (mute == APP.RTC.localVideo.isMuted())
|
|
||||||
{
|
|
||||||
APP.xmpp.sendVideoInfoPresence(mute);
|
|
||||||
if (callback)
|
|
||||||
callback(mute);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
APP.RTC.localVideo.setMute(mute);
|
|
||||||
APP.xmpp.setVideoMute(
|
|
||||||
mute,
|
|
||||||
callback,
|
|
||||||
options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setDeviceAvailability: function (devices) {
|
|
||||||
if(!devices)
|
|
||||||
return;
|
|
||||||
if(devices.audio === true || devices.audio === false)
|
|
||||||
this.devices.audio = devices.audio;
|
|
||||||
if(devices.video === true || devices.video === false)
|
|
||||||
this.devices.video = devices.video;
|
|
||||||
eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, this.devices);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* A method to handle stopping of the stream.
|
|
||||||
* One point to handle the differences in various implementations.
|
|
||||||
* @param mediaStream MediaStream object to stop.
|
|
||||||
*/
|
|
||||||
stopMediaStream: function (mediaStream) {
|
|
||||||
mediaStream.getTracks().forEach(function (track) {
|
|
||||||
// stop() not supported with IE
|
|
||||||
if (track.stop) {
|
|
||||||
track.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// leave stop for implementation still using it
|
|
||||||
if (mediaStream.stop) {
|
|
||||||
mediaStream.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Adds onended/inactive handler to a MediaStream.
|
|
||||||
* @param mediaStream a MediaStream to attach onended/inactive handler
|
|
||||||
* @param handler the handler
|
|
||||||
*/
|
|
||||||
addMediaStreamInactiveHandler: function (mediaStream, handler) {
|
|
||||||
if(RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
// themasys
|
|
||||||
mediaStream.attachEvent('ended', function () {
|
|
||||||
handler(mediaStream);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if(typeof mediaStream.active !== "undefined")
|
|
||||||
mediaStream.oninactive = handler;
|
|
||||||
else
|
|
||||||
mediaStream.onended = handler;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Removes onended/inactive handler.
|
|
||||||
* @param mediaStream the MediaStream to remove the handler from.
|
|
||||||
* @param handler the handler to remove.
|
|
||||||
*/
|
|
||||||
removeMediaStreamInactiveHandler: function (mediaStream, handler) {
|
|
||||||
if(RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
// themasys
|
|
||||||
mediaStream.detachEvent('ended', handler);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if(typeof mediaStream.active !== "undefined")
|
|
||||||
mediaStream.oninactive = null;
|
|
||||||
else
|
|
||||||
mediaStream.onended = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = RTC;
|
|
|
@ -1,576 +0,0 @@
|
||||||
/* global APP, config, require, attachMediaStream, getUserMedia,
|
|
||||||
RTCPeerConnection, webkitMediaStream, webkitURL, webkitRTCPeerConnection,
|
|
||||||
mozRTCIceCandidate, mozRTCSessionDescription, mozRTCPeerConnection */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var Resolutions = require("../../service/RTC/Resolutions");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var AdapterJS = require("./adapter.screenshare");
|
|
||||||
|
|
||||||
var currentResolution = null;
|
|
||||||
|
|
||||||
function getPreviousResolution(resolution) {
|
|
||||||
if(!Resolutions[resolution])
|
|
||||||
return null;
|
|
||||||
var order = Resolutions[resolution].order;
|
|
||||||
var res = null;
|
|
||||||
var resName = null;
|
|
||||||
for(var i in Resolutions) {
|
|
||||||
var tmp = Resolutions[i];
|
|
||||||
if (!res || (res.order < tmp.order && tmp.order < order)) {
|
|
||||||
resName = i;
|
|
||||||
res = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setResolutionConstraints(constraints, resolution) {
|
|
||||||
var isAndroid = RTCBrowserType.isAndroid();
|
|
||||||
|
|
||||||
if (Resolutions[resolution]) {
|
|
||||||
constraints.video.mandatory.minWidth = Resolutions[resolution].width;
|
|
||||||
constraints.video.mandatory.minHeight = Resolutions[resolution].height;
|
|
||||||
}
|
|
||||||
else if (isAndroid) {
|
|
||||||
// FIXME can't remember if the purpose of this was to always request
|
|
||||||
// low resolution on Android ? if yes it should be moved up front
|
|
||||||
constraints.video.mandatory.minWidth = 320;
|
|
||||||
constraints.video.mandatory.minHeight = 240;
|
|
||||||
constraints.video.mandatory.maxFrameRate = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constraints.video.mandatory.minWidth)
|
|
||||||
constraints.video.mandatory.maxWidth =
|
|
||||||
constraints.video.mandatory.minWidth;
|
|
||||||
if (constraints.video.mandatory.minHeight)
|
|
||||||
constraints.video.mandatory.maxHeight =
|
|
||||||
constraints.video.mandatory.minHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConstraints(um, resolution, bandwidth, fps, desktopStream) {
|
|
||||||
var constraints = {audio: false, video: false};
|
|
||||||
|
|
||||||
if (um.indexOf('video') >= 0) {
|
|
||||||
// same behaviour as true
|
|
||||||
constraints.video = { mandatory: {}, optional: [] };
|
|
||||||
|
|
||||||
constraints.video.optional.push({ googLeakyBucket: true });
|
|
||||||
|
|
||||||
setResolutionConstraints(constraints, resolution);
|
|
||||||
}
|
|
||||||
if (um.indexOf('audio') >= 0) {
|
|
||||||
if (!RTCBrowserType.isFirefox()) {
|
|
||||||
// same behaviour as true
|
|
||||||
constraints.audio = { mandatory: {}, optional: []};
|
|
||||||
// if it is good enough for hangouts...
|
|
||||||
constraints.audio.optional.push(
|
|
||||||
{googEchoCancellation: true},
|
|
||||||
{googAutoGainControl: true},
|
|
||||||
{googNoiseSupression: true},
|
|
||||||
{googHighpassFilter: true},
|
|
||||||
{googNoisesuppression2: true},
|
|
||||||
{googEchoCancellation2: true},
|
|
||||||
{googAutoGainControl2: true}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
constraints.audio = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (um.indexOf('screen') >= 0) {
|
|
||||||
if (RTCBrowserType.isChrome()) {
|
|
||||||
constraints.video = {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: "screen",
|
|
||||||
googLeakyBucket: true,
|
|
||||||
maxWidth: window.screen.width,
|
|
||||||
maxHeight: window.screen.height,
|
|
||||||
maxFrameRate: 3
|
|
||||||
},
|
|
||||||
optional: []
|
|
||||||
};
|
|
||||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
constraints.video = {
|
|
||||||
optional: [
|
|
||||||
{
|
|
||||||
sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
} else if (RTCBrowserType.isFirefox()) {
|
|
||||||
constraints.video = {
|
|
||||||
mozMediaSource: "window",
|
|
||||||
mediaSource: "window"
|
|
||||||
};
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"'screen' WebRTC media source is supported only in Chrome" +
|
|
||||||
" and with Temasys plugin");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (um.indexOf('desktop') >= 0) {
|
|
||||||
constraints.video = {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: "desktop",
|
|
||||||
chromeMediaSourceId: desktopStream,
|
|
||||||
googLeakyBucket: true,
|
|
||||||
maxWidth: window.screen.width,
|
|
||||||
maxHeight: window.screen.height,
|
|
||||||
maxFrameRate: 3
|
|
||||||
},
|
|
||||||
optional: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bandwidth) {
|
|
||||||
if (!constraints.video) {
|
|
||||||
//same behaviour as true
|
|
||||||
constraints.video = {mandatory: {}, optional: []};
|
|
||||||
}
|
|
||||||
constraints.video.optional.push({bandwidth: bandwidth});
|
|
||||||
}
|
|
||||||
if (fps) {
|
|
||||||
// for some cameras it might be necessary to request 30fps
|
|
||||||
// so they choose 30fps mjpg over 10fps yuy2
|
|
||||||
if (!constraints.video) {
|
|
||||||
// same behaviour as true;
|
|
||||||
constraints.video = {mandatory: {}, optional: []};
|
|
||||||
}
|
|
||||||
constraints.video.mandatory.minFrameRate = fps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we turn audio for both audio and video tracks, the fake audio & video seems to work
|
|
||||||
// only when enabled in one getUserMedia call, we cannot get fake audio separate by fake video
|
|
||||||
// this later can be a problem with some of the tests
|
|
||||||
if(RTCBrowserType.isFirefox() && config.firefox_fake_device)
|
|
||||||
{
|
|
||||||
// seems to be fixed now, removing this experimental fix, as having
|
|
||||||
// multiple audio tracks brake the tests
|
|
||||||
//constraints.audio = true;
|
|
||||||
constraints.fake = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return constraints;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function RTCUtils(RTCService, eventEmitter, onTemasysPluginReady)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
this.service = RTCService;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
var FFversion = RTCBrowserType.getFirefoxVersion();
|
|
||||||
if (FFversion >= 40) {
|
|
||||||
this.peerconnection = mozRTCPeerConnection;
|
|
||||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
|
||||||
this.pc_constraints = {};
|
|
||||||
this.attachMediaStream = function (element, stream) {
|
|
||||||
// srcObject is being standardized and FF will eventually
|
|
||||||
// support that unprefixed. FF also supports the
|
|
||||||
// "element.src = URL.createObjectURL(...)" combo, but that
|
|
||||||
// will be deprecated in favour of srcObject.
|
|
||||||
//
|
|
||||||
// https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
|
|
||||||
// https://github.com/webrtc/samples/issues/302
|
|
||||||
if(!element[0])
|
|
||||||
return;
|
|
||||||
element[0].mozSrcObject = stream;
|
|
||||||
element[0].play();
|
|
||||||
};
|
|
||||||
this.getStreamID = function (stream) {
|
|
||||||
var id = stream.id;
|
|
||||||
if (!id) {
|
|
||||||
var tracks = stream.getVideoTracks();
|
|
||||||
if (!tracks || tracks.length === 0) {
|
|
||||||
tracks = stream.getAudioTracks();
|
|
||||||
}
|
|
||||||
id = tracks[0].id;
|
|
||||||
}
|
|
||||||
return APP.xmpp.filter_special_chars(id);
|
|
||||||
};
|
|
||||||
this.getVideoSrc = function (element) {
|
|
||||||
if(!element)
|
|
||||||
return null;
|
|
||||||
return element.mozSrcObject;
|
|
||||||
};
|
|
||||||
this.setVideoSrc = function (element, src) {
|
|
||||||
if(element)
|
|
||||||
element.mozSrcObject = src;
|
|
||||||
};
|
|
||||||
window.RTCSessionDescription = mozRTCSessionDescription;
|
|
||||||
window.RTCIceCandidate = mozRTCIceCandidate;
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Firefox version too old: " + FFversion + ". Required >= 40.");
|
|
||||||
window.location.href = 'unsupported_browser.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
|
|
||||||
this.peerconnection = webkitRTCPeerConnection;
|
|
||||||
this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
|
||||||
this.attachMediaStream = function (element, stream) {
|
|
||||||
element.attr('src', webkitURL.createObjectURL(stream));
|
|
||||||
};
|
|
||||||
this.getStreamID = function (stream) {
|
|
||||||
// streams from FF endpoints have the characters '{' and '}'
|
|
||||||
// that make jQuery choke.
|
|
||||||
return APP.xmpp.filter_special_chars(stream.id);
|
|
||||||
};
|
|
||||||
this.getVideoSrc = function (element) {
|
|
||||||
if(!element)
|
|
||||||
return null;
|
|
||||||
return element.getAttribute("src");
|
|
||||||
};
|
|
||||||
this.setVideoSrc = function (element, src) {
|
|
||||||
if(element)
|
|
||||||
element.setAttribute("src", src);
|
|
||||||
};
|
|
||||||
// DTLS should now be enabled by default but..
|
|
||||||
this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
|
|
||||||
if (RTCBrowserType.isAndroid()) {
|
|
||||||
this.pc_constraints = {}; // disable DTLS on Android
|
|
||||||
}
|
|
||||||
if (!webkitMediaStream.prototype.getVideoTracks) {
|
|
||||||
webkitMediaStream.prototype.getVideoTracks = function () {
|
|
||||||
return this.videoTracks;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!webkitMediaStream.prototype.getAudioTracks) {
|
|
||||||
webkitMediaStream.prototype.getAudioTracks = function () {
|
|
||||||
return this.audioTracks;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Detect IE/Safari
|
|
||||||
else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
|
|
||||||
//AdapterJS.WebRTCPlugin.setLogLevel(
|
|
||||||
// AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
|
|
||||||
|
|
||||||
AdapterJS.webRTCReady(function (isPlugin) {
|
|
||||||
|
|
||||||
self.peerconnection = RTCPeerConnection;
|
|
||||||
self.getUserMedia = getUserMedia;
|
|
||||||
self.attachMediaStream = function (elSel, stream) {
|
|
||||||
|
|
||||||
if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachMediaStream(elSel[0], stream);
|
|
||||||
};
|
|
||||||
self.getStreamID = function (stream) {
|
|
||||||
return APP.xmpp.filter_special_chars(stream.label);
|
|
||||||
};
|
|
||||||
self.getVideoSrc = function (element) {
|
|
||||||
if (!element) {
|
|
||||||
console.warn("Attempt to get video SRC of null element");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var children = element.children;
|
|
||||||
for (var i = 0; i !== children.length; ++i) {
|
|
||||||
if (children[i].name === 'streamId') {
|
|
||||||
return children[i].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//console.info(element.id + " SRC: " + src);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
self.setVideoSrc = function (element, src) {
|
|
||||||
//console.info("Set video src: ", element, src);
|
|
||||||
if (!src) {
|
|
||||||
console.warn("Not attaching video stream, 'src' is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AdapterJS.WebRTCPlugin.WaitForPluginReady();
|
|
||||||
var stream = AdapterJS.WebRTCPlugin.plugin
|
|
||||||
.getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
|
|
||||||
attachMediaStream(element, stream);
|
|
||||||
};
|
|
||||||
|
|
||||||
onTemasysPluginReady(isPlugin);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
console.log('Browser does not appear to be WebRTC-capable');
|
|
||||||
} catch (e) { }
|
|
||||||
window.location.href = 'unsupported_browser.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|
||||||
um, success_callback, failure_callback, resolution,bandwidth, fps,
|
|
||||||
desktopStream) {
|
|
||||||
currentResolution = resolution;
|
|
||||||
|
|
||||||
var constraints = getConstraints(
|
|
||||||
um, resolution, bandwidth, fps, desktopStream);
|
|
||||||
|
|
||||||
console.info("Get media constraints", constraints);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.getUserMedia(constraints,
|
|
||||||
function (stream) {
|
|
||||||
console.log('onUserMediaSuccess');
|
|
||||||
self.setAvailableDevices(um, true);
|
|
||||||
success_callback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.setAvailableDevices(um, false);
|
|
||||||
console.warn('Failed to get access to local media. Error ',
|
|
||||||
error, constraints);
|
|
||||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, error);
|
|
||||||
if (failure_callback) {
|
|
||||||
failure_callback(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('GUM failed: ', e);
|
|
||||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, e);
|
|
||||||
if(failure_callback) {
|
|
||||||
failure_callback(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.setAvailableDevices = function (um, available) {
|
|
||||||
var devices = {};
|
|
||||||
if(um.indexOf("video") != -1) {
|
|
||||||
devices.video = available;
|
|
||||||
}
|
|
||||||
if(um.indexOf("audio") != -1) {
|
|
||||||
devices.audio = available;
|
|
||||||
}
|
|
||||||
this.service.setDeviceAvailability(devices);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We ask for audio and video combined stream in order to get permissions and
|
|
||||||
* not to ask twice.
|
|
||||||
*/
|
|
||||||
RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
|
||||||
function(devices, callback, usageOptions)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
// Get AV
|
|
||||||
|
|
||||||
var successCallback = function (stream) {
|
|
||||||
if(callback)
|
|
||||||
callback(stream, usageOptions);
|
|
||||||
else
|
|
||||||
self.successCallback(stream, usageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!devices)
|
|
||||||
devices = ['audio', 'video'];
|
|
||||||
|
|
||||||
var newDevices = [];
|
|
||||||
|
|
||||||
|
|
||||||
if(usageOptions)
|
|
||||||
for(var i = 0; i < devices.length; i++) {
|
|
||||||
var device = devices[i];
|
|
||||||
if(usageOptions[device] === true)
|
|
||||||
newDevices.push(device);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
newDevices = devices;
|
|
||||||
|
|
||||||
if(newDevices.length === 0) {
|
|
||||||
successCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
|
|
||||||
// With FF/IE we can't split the stream into audio and video because FF
|
|
||||||
// doesn't support media stream constructors. So, we need to get the
|
|
||||||
// audio stream separately from the video stream using two distinct GUM
|
|
||||||
// calls. Not very user friendly :-( but we don't have many other
|
|
||||||
// options neither.
|
|
||||||
//
|
|
||||||
// Note that we pack those 2 streams in a single object and pass it to
|
|
||||||
// the successCallback method.
|
|
||||||
var obtainVideo = function (audioStream) {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['video'],
|
|
||||||
function (videoStream) {
|
|
||||||
return successCallback({
|
|
||||||
audioStream: audioStream,
|
|
||||||
videoStream: videoStream
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error(
|
|
||||||
'failed to obtain video stream - stop', error);
|
|
||||||
self.errorCallback(error);
|
|
||||||
},
|
|
||||||
config.resolution || '360');
|
|
||||||
};
|
|
||||||
var obtainAudio = function () {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['audio'],
|
|
||||||
function (audioStream) {
|
|
||||||
if (newDevices.indexOf('video') !== -1)
|
|
||||||
obtainVideo(audioStream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error(
|
|
||||||
'failed to obtain audio stream - stop', error);
|
|
||||||
self.errorCallback(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (newDevices.indexOf('audio') !== -1) {
|
|
||||||
obtainAudio();
|
|
||||||
} else {
|
|
||||||
obtainVideo(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.getUserMediaWithConstraints(
|
|
||||||
newDevices,
|
|
||||||
function (stream) {
|
|
||||||
successCallback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.errorCallback(error);
|
|
||||||
},
|
|
||||||
config.resolution || '360');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.successCallback = function (stream, usageOptions) {
|
|
||||||
// If this is FF or IE, the stream parameter is *not* a MediaStream object,
|
|
||||||
// it's an object with two properties: audioStream, videoStream.
|
|
||||||
if (stream && stream.getAudioTracks && stream.getVideoTracks)
|
|
||||||
console.log('got', stream, stream.getAudioTracks().length,
|
|
||||||
stream.getVideoTracks().length);
|
|
||||||
this.handleLocalStream(stream, usageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.errorCallback = function (error) {
|
|
||||||
var self = this;
|
|
||||||
console.error('failed to obtain audio/video stream - trying audio only', error);
|
|
||||||
var resolution = getPreviousResolution(currentResolution);
|
|
||||||
if(typeof error == "object" && error.constraintName && error.name
|
|
||||||
&& (error.name == "ConstraintNotSatisfiedError" ||
|
|
||||||
error.name == "OverconstrainedError") &&
|
|
||||||
(error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
|
|
||||||
error.constraintName == "minHeight" || error.constraintName == "maxHeight")
|
|
||||||
&& resolution)
|
|
||||||
{
|
|
||||||
self.getUserMediaWithConstraints(['audio', 'video'],
|
|
||||||
function (stream) {
|
|
||||||
return self.successCallback(stream);
|
|
||||||
}, function (error) {
|
|
||||||
return self.errorCallback(error);
|
|
||||||
}, resolution);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['audio'],
|
|
||||||
function (stream) {
|
|
||||||
return self.successCallback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error('failed to obtain audio/video stream - stop',
|
|
||||||
error);
|
|
||||||
return self.successCallback(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.handleLocalStream = function(stream, usageOptions) {
|
|
||||||
// If this is FF, the stream parameter is *not* a MediaStream object, it's
|
|
||||||
// an object with two properties: audioStream, videoStream.
|
|
||||||
var audioStream, videoStream;
|
|
||||||
if(window.webkitMediaStream)
|
|
||||||
{
|
|
||||||
audioStream = new webkitMediaStream();
|
|
||||||
videoStream = new webkitMediaStream();
|
|
||||||
if(stream) {
|
|
||||||
var audioTracks = stream.getAudioTracks();
|
|
||||||
|
|
||||||
for (var i = 0; i < audioTracks.length; i++) {
|
|
||||||
audioStream.addTrack(audioTracks[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoTracks = stream.getVideoTracks();
|
|
||||||
|
|
||||||
for (i = 0; i < videoTracks.length; i++) {
|
|
||||||
videoStream.addTrack(videoTracks[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
|
|
||||||
{ // Firefox and Temasys plugin
|
|
||||||
if (stream && stream.audioStream)
|
|
||||||
audioStream = stream.audioStream;
|
|
||||||
else
|
|
||||||
audioStream = new DummyMediaStream("dummyAudio");
|
|
||||||
|
|
||||||
if (stream && stream.videoStream)
|
|
||||||
videoStream = stream.videoStream;
|
|
||||||
else
|
|
||||||
videoStream = new DummyMediaStream("dummyVideo");
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioMuted = (usageOptions && usageOptions.audio === false),
|
|
||||||
videoMuted = (usageOptions && usageOptions.video === false);
|
|
||||||
|
|
||||||
var audioGUM = (!usageOptions || usageOptions.audio !== false),
|
|
||||||
videoGUM = (!usageOptions || usageOptions.video !== false);
|
|
||||||
|
|
||||||
|
|
||||||
this.service.createLocalStream(
|
|
||||||
audioStream, MediaStreamType.AUDIO_TYPE, null, null,
|
|
||||||
audioMuted, audioGUM);
|
|
||||||
|
|
||||||
this.service.createLocalStream(
|
|
||||||
videoStream, MediaStreamType.VIDEO_TYPE, null, 'camera',
|
|
||||||
videoMuted, videoGUM);
|
|
||||||
};
|
|
||||||
|
|
||||||
function DummyMediaStream(id) {
|
|
||||||
this.id = id;
|
|
||||||
this.label = id;
|
|
||||||
this.stop = function() { };
|
|
||||||
this.getAudioTracks = function() { return []; };
|
|
||||||
this.getVideoTracks = function() { return []; };
|
|
||||||
}
|
|
||||||
|
|
||||||
RTCUtils.prototype.createStream = function(stream, isVideo) {
|
|
||||||
var newStream = null;
|
|
||||||
if (window.webkitMediaStream) {
|
|
||||||
newStream = new webkitMediaStream();
|
|
||||||
if (newStream) {
|
|
||||||
var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
|
|
||||||
|
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
|
||||||
newStream.addTrack(tracks[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// FIXME: this is duplicated with 'handleLocalStream' !!!
|
|
||||||
if (stream) {
|
|
||||||
newStream = stream;
|
|
||||||
} else {
|
|
||||||
newStream =
|
|
||||||
new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = RTCUtils;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,9 @@
|
||||||
/* global $, config, interfaceConfig */
|
/* global $, APP, config, interfaceConfig */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Yana Stamcheva on 2/10/15.
|
* Created by Yana Stamcheva on 2/10/15.
|
||||||
*/
|
*/
|
||||||
var messageHandler = require("./util/MessageHandler");
|
var messageHandler = require("./util/MessageHandler");
|
||||||
var callStats = require("../statistics/CallStats");
|
|
||||||
var APP = require("../../app");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the html for the overall feedback window.
|
* Constructs the html for the overall feedback window.
|
||||||
|
@ -79,7 +77,7 @@ var Feedback = {
|
||||||
init: function () {
|
init: function () {
|
||||||
// CallStats is the way we send feedback, so we don't have to initialise
|
// CallStats is the way we send feedback, so we don't have to initialise
|
||||||
// if callstats isn't enabled.
|
// if callstats isn't enabled.
|
||||||
if (!this.isEnabled())
|
if (!APP.conference.isCallstatsEnabled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$("div.feedbackButton").css("display", "block");
|
$("div.feedbackButton").css("display", "block");
|
||||||
|
@ -93,10 +91,7 @@ var Feedback = {
|
||||||
* @return true if the feedback functionality is enabled, false otherwise.
|
* @return true if the feedback functionality is enabled, false otherwise.
|
||||||
*/
|
*/
|
||||||
isEnabled: function() {
|
isEnabled: function() {
|
||||||
// XXX callStats.isEnabled() indicates whether we are allowed to attempt
|
return APP.conference.isCallstatsEnabled();
|
||||||
// to integrate callstats.io. Whether our attempt was/is/will be
|
|
||||||
// successful is a different question.
|
|
||||||
return callStats.isEnabled();
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Opens the feedback window.
|
* Opens the feedback window.
|
||||||
|
@ -122,7 +117,7 @@ var Feedback = {
|
||||||
// If the feedback is less than 3 stars we're going to
|
// If the feedback is less than 3 stars we're going to
|
||||||
// ask the user for more information.
|
// ask the user for more information.
|
||||||
if (Feedback.feedbackScore > 3) {
|
if (Feedback.feedbackScore > 3) {
|
||||||
callStats.sendFeedback(Feedback.feedbackScore, "");
|
APP.conference.sendFeedback(Feedback.feedbackScore, "");
|
||||||
if (feedbackWindowCallback)
|
if (feedbackWindowCallback)
|
||||||
feedbackWindowCallback();
|
feedbackWindowCallback();
|
||||||
else
|
else
|
||||||
|
@ -164,7 +159,7 @@ var Feedback = {
|
||||||
= document.getElementById("feedbackTextArea").value;
|
= document.getElementById("feedbackTextArea").value;
|
||||||
|
|
||||||
if (feedbackDetails && feedbackDetails.length > 0)
|
if (feedbackDetails && feedbackDetails.length > 0)
|
||||||
callStats.sendFeedback( Feedback.feedbackScore,
|
APP.conference.sendFeedback( Feedback.feedbackScore,
|
||||||
feedbackDetails);
|
feedbackDetails);
|
||||||
|
|
||||||
if (feedbackWindowCallback)
|
if (feedbackWindowCallback)
|
||||||
|
|
1383
modules/UI/UI.js
1383
modules/UI/UI.js
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,17 @@
|
||||||
/* global APP, interfaceConfig, $, Strophe */
|
/* global APP, interfaceConfig, $ */
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
var CanvasUtil = require("./CanvasUtils");
|
|
||||||
|
|
||||||
var ASDrawContext = null;
|
import CanvasUtil from './CanvasUtils';
|
||||||
|
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||||
|
|
||||||
function initActiveSpeakerAudioLevels() {
|
const LOCAL_LEVEL = 'local';
|
||||||
var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
|
|
||||||
var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
|
let ASDrawContext = null;
|
||||||
|
let audioLevelCanvasCache = {};
|
||||||
|
|
||||||
|
function initDominantSpeakerAudioLevels() {
|
||||||
|
let ASRadius = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE / 2;
|
||||||
|
let ASCenter = (interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
|
||||||
|
|
||||||
// Draw a circle.
|
// Draw a circle.
|
||||||
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
|
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
|
||||||
|
@ -18,148 +23,25 @@ function initActiveSpeakerAudioLevels() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The audio Levels plugin.
|
|
||||||
*/
|
|
||||||
var AudioLevels = (function(my) {
|
|
||||||
var audioLevelCanvasCache = {};
|
|
||||||
|
|
||||||
my.LOCAL_LEVEL = 'local';
|
|
||||||
|
|
||||||
my.init = function () {
|
|
||||||
ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d');
|
|
||||||
initActiveSpeakerAudioLevels();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the audio level canvas for the given peerJid. If the canvas
|
|
||||||
* didn't exist we create it.
|
|
||||||
*/
|
|
||||||
my.updateAudioLevelCanvas = function (peerJid, VideoLayout) {
|
|
||||||
var resourceJid = null;
|
|
||||||
var videoSpanId = null;
|
|
||||||
if (!peerJid)
|
|
||||||
videoSpanId = 'localVideoContainer';
|
|
||||||
else {
|
|
||||||
resourceJid = Strophe.getResourceFromJid(peerJid);
|
|
||||||
|
|
||||||
videoSpanId = 'participant_' + resourceJid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoSpan = document.getElementById(videoSpanId);
|
|
||||||
|
|
||||||
if (!videoSpan) {
|
|
||||||
if (resourceJid)
|
|
||||||
console.error("No video element for jid", resourceJid);
|
|
||||||
else
|
|
||||||
console.error("No video element for local video.");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
|
|
||||||
|
|
||||||
var videoSpaceWidth = $('#remoteVideos').width();
|
|
||||||
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
|
|
||||||
var thumbnailWidth = thumbnailSize[0];
|
|
||||||
var thumbnailHeight = thumbnailSize[1];
|
|
||||||
|
|
||||||
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
|
|
||||||
|
|
||||||
audioLevelCanvas = document.createElement('canvas');
|
|
||||||
audioLevelCanvas.className = "audiolevel";
|
|
||||||
audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
|
|
||||||
audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
|
|
||||||
resizeAudioLevelCanvas( audioLevelCanvas,
|
|
||||||
thumbnailWidth,
|
|
||||||
thumbnailHeight);
|
|
||||||
|
|
||||||
videoSpan.appendChild(audioLevelCanvas);
|
|
||||||
} else {
|
|
||||||
audioLevelCanvas = audioLevelCanvas.get(0);
|
|
||||||
|
|
||||||
resizeAudioLevelCanvas( audioLevelCanvas,
|
|
||||||
thumbnailWidth,
|
|
||||||
thumbnailHeight);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the audio level UI for the given resourceJid.
|
|
||||||
*
|
|
||||||
* @param resourceJid the resource jid indicating the video element for
|
|
||||||
* which we draw the audio level
|
|
||||||
* @param audioLevel the newAudio level to render
|
|
||||||
*/
|
|
||||||
my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) {
|
|
||||||
drawAudioLevelCanvas(resourceJid, audioLevel);
|
|
||||||
|
|
||||||
var videoSpanId = getVideoSpanId(resourceJid);
|
|
||||||
|
|
||||||
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
|
|
||||||
|
|
||||||
if (!audioLevelCanvas)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var drawContext = audioLevelCanvas.getContext('2d');
|
|
||||||
|
|
||||||
var canvasCache = audioLevelCanvasCache[resourceJid];
|
|
||||||
|
|
||||||
drawContext.clearRect (0, 0,
|
|
||||||
audioLevelCanvas.width, audioLevelCanvas.height);
|
|
||||||
drawContext.drawImage(canvasCache, 0, 0);
|
|
||||||
|
|
||||||
if(resourceJid === AudioLevels.LOCAL_LEVEL) {
|
|
||||||
if(!APP.xmpp.myJid()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resourceJid = APP.xmpp.myResource();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(resourceJid === largeVideoResourceJid) {
|
|
||||||
window.requestAnimationFrame(function () {
|
|
||||||
AudioLevels.updateActiveSpeakerAudioLevel(audioLevel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
my.updateActiveSpeakerAudioLevel = function(audioLevel) {
|
|
||||||
if($("#activeSpeaker").css("visibility") == "hidden" || ASDrawContext === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ASDrawContext.clearRect(0, 0, 300, 300);
|
|
||||||
if (!audioLevel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
|
|
||||||
|
|
||||||
|
|
||||||
// Fill the shape.
|
|
||||||
ASDrawContext.fill();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes the given audio level canvas to match the given thumbnail size.
|
* Resizes the given audio level canvas to match the given thumbnail size.
|
||||||
*/
|
*/
|
||||||
function resizeAudioLevelCanvas(audioLevelCanvas,
|
function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
|
||||||
thumbnailWidth,
|
|
||||||
thumbnailHeight) {
|
|
||||||
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
|
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
|
||||||
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
|
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the audio level canvas into the cached canvas object.
|
* Draws the audio level canvas into the cached canvas object.
|
||||||
*
|
*
|
||||||
* @param resourceJid the resource jid indicating the video element for
|
* @param id of the user for whom we draw the audio level
|
||||||
* which we draw the audio level
|
|
||||||
* @param audioLevel the newAudio level to render
|
* @param audioLevel the newAudio level to render
|
||||||
*/
|
*/
|
||||||
function drawAudioLevelCanvas(resourceJid, audioLevel) {
|
function drawAudioLevelCanvas(id, audioLevel) {
|
||||||
if (!audioLevelCanvasCache[resourceJid]) {
|
if (!audioLevelCanvasCache[id]) {
|
||||||
|
|
||||||
var videoSpanId = getVideoSpanId(resourceJid);
|
let videoSpanId = getVideoSpanId(id);
|
||||||
|
|
||||||
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
|
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
|
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
|
||||||
|
@ -168,21 +50,21 @@ var AudioLevels = (function(my) {
|
||||||
* been observed to pile into the console, strain the CPU.
|
* been observed to pile into the console, strain the CPU.
|
||||||
*/
|
*/
|
||||||
if (audioLevelCanvasOrig) {
|
if (audioLevelCanvasOrig) {
|
||||||
audioLevelCanvasCache[resourceJid] =
|
audioLevelCanvasCache[id] = CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
|
||||||
CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var canvas = audioLevelCanvasCache[resourceJid];
|
let canvas = audioLevelCanvasCache[id];
|
||||||
|
|
||||||
if (!canvas)
|
if (!canvas) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var drawContext = canvas.getContext('2d');
|
let drawContext = canvas.getContext('2d');
|
||||||
|
|
||||||
drawContext.clearRect(0, 0, canvas.width, canvas.height);
|
drawContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
var shadowLevel = getShadowLevel(audioLevel);
|
let shadowLevel = getShadowLevel(audioLevel);
|
||||||
|
|
||||||
if (shadowLevel > 0) {
|
if (shadowLevel > 0) {
|
||||||
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
|
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
|
||||||
|
@ -194,73 +76,160 @@ var AudioLevels = (function(my) {
|
||||||
interfaceConfig.SHADOW_COLOR,
|
interfaceConfig.SHADOW_COLOR,
|
||||||
shadowLevel);
|
shadowLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the shadow/glow level for the given audio level.
|
* Returns the shadow/glow level for the given audio level.
|
||||||
*
|
*
|
||||||
* @param audioLevel the audio level from which we determine the shadow
|
* @param audioLevel the audio level from which we determine the shadow
|
||||||
* level
|
* level
|
||||||
*/
|
*/
|
||||||
function getShadowLevel (audioLevel) {
|
function getShadowLevel (audioLevel) {
|
||||||
var shadowLevel = 0;
|
let shadowLevel = 0;
|
||||||
|
|
||||||
if (audioLevel <= 0.3) {
|
if (audioLevel <= 0.3) {
|
||||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
|
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
|
||||||
}
|
} else if (audioLevel <= 0.6) {
|
||||||
else if (audioLevel <= 0.6) {
|
|
||||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
|
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
|
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
|
||||||
}
|
}
|
||||||
return shadowLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return shadowLevel;
|
||||||
* Returns the video span id corresponding to the given resourceJid or local
|
}
|
||||||
* user.
|
|
||||||
|
/**
|
||||||
|
* Returns the video span id corresponding to the given user id
|
||||||
*/
|
*/
|
||||||
function getVideoSpanId(resourceJid) {
|
function getVideoSpanId(id) {
|
||||||
var videoSpanId = null;
|
let videoSpanId = null;
|
||||||
if (resourceJid === AudioLevels.LOCAL_LEVEL ||
|
|
||||||
(APP.xmpp.myResource() && resourceJid === APP.xmpp.myResource()))
|
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
|
||||||
videoSpanId = 'localVideoContainer';
|
videoSpanId = 'localVideoContainer';
|
||||||
else
|
} else {
|
||||||
videoSpanId = 'participant_' + resourceJid;
|
videoSpanId = `participant_${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
return videoSpanId;
|
return videoSpanId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The audio Levels plugin.
|
||||||
|
*/
|
||||||
|
const AudioLevels = {
|
||||||
|
|
||||||
|
init () {
|
||||||
|
ASDrawContext = $('#dominantSpeakerAudioLevel')[0].getContext('2d');
|
||||||
|
initDominantSpeakerAudioLevels();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the remote video has been resized.
|
* Updates the audio level canvas for the given id. If the canvas
|
||||||
|
* didn't exist we create it.
|
||||||
*/
|
*/
|
||||||
$(document).bind('remotevideo.resized', function (event, width, height) {
|
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
|
||||||
var resized = false;
|
let videoSpanId = 'localVideoContainer';
|
||||||
$('#remoteVideos>span>canvas').each(function() {
|
if (id) {
|
||||||
var canvas = $(this).get(0);
|
videoSpanId = `participant_${id}`;
|
||||||
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
|
|
||||||
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
|
|
||||||
resized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) {
|
let videoSpan = document.getElementById(videoSpanId);
|
||||||
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
|
|
||||||
resized = true;
|
if (!videoSpan) {
|
||||||
|
if (id) {
|
||||||
|
console.error("No video element for id", id);
|
||||||
|
} else {
|
||||||
|
console.error("No video element for local video.");
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
|
||||||
|
|
||||||
|
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
|
||||||
|
|
||||||
|
audioLevelCanvas = document.createElement('canvas');
|
||||||
|
audioLevelCanvas.className = "audiolevel";
|
||||||
|
audioLevelCanvas.style.bottom = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
|
||||||
|
audioLevelCanvas.style.left = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
|
||||||
|
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
|
||||||
|
|
||||||
|
videoSpan.appendChild(audioLevelCanvas);
|
||||||
|
} else {
|
||||||
|
audioLevelCanvas = audioLevelCanvas.get(0);
|
||||||
|
|
||||||
|
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio level UI for the given id.
|
||||||
|
*
|
||||||
|
* @param id id of the user for whom we draw the audio level
|
||||||
|
* @param audioLevel the newAudio level to render
|
||||||
|
*/
|
||||||
|
updateAudioLevel (id, audioLevel, largeVideoId) {
|
||||||
|
drawAudioLevelCanvas(id, audioLevel);
|
||||||
|
|
||||||
|
let videoSpanId = getVideoSpanId(id);
|
||||||
|
|
||||||
|
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
|
||||||
|
|
||||||
|
if (!audioLevelCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawContext = audioLevelCanvas.getContext('2d');
|
||||||
|
|
||||||
|
let canvasCache = audioLevelCanvasCache[id];
|
||||||
|
|
||||||
|
drawContext.clearRect(
|
||||||
|
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
|
||||||
|
);
|
||||||
|
drawContext.drawImage(canvasCache, 0, 0);
|
||||||
|
|
||||||
|
if (id === LOCAL_LEVEL) {
|
||||||
|
id = APP.conference.localId;
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(id === largeVideoId) {
|
||||||
|
window.requestAnimationFrame(function () {
|
||||||
|
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (resized)
|
updateDominantSpeakerAudioLevel (audioLevel) {
|
||||||
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
|
if($("#domiantSpeaker").css("visibility") == "hidden" || ASDrawContext === null) {
|
||||||
audioLevelCanvasCache[resourceJid].width =
|
return;
|
||||||
width + interfaceConfig.CANVAS_EXTRA;
|
}
|
||||||
audioLevelCanvasCache[resourceJid].height =
|
|
||||||
height + interfaceConfig.CANVAS_EXTRA;
|
ASDrawContext.clearRect(0, 0, 300, 300);
|
||||||
});
|
if (!audioLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
|
||||||
|
|
||||||
|
|
||||||
|
// Fill the shape.
|
||||||
|
ASDrawContext.fill();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCanvasSize (thumbWidth, thumbHeight) {
|
||||||
|
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
|
||||||
|
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
|
||||||
|
|
||||||
|
BottomToolbar.getThumbs().children('canvas').width(canvasWidth).height(canvasHeight);
|
||||||
|
|
||||||
|
Object.keys(audioLevelCanvasCache).forEach(function (id) {
|
||||||
|
audioLevelCanvasCache[id].width = canvasWidth;
|
||||||
|
audioLevelCanvasCache[id].height = canvasHeight;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return my;
|
export default AudioLevels;
|
||||||
|
|
||||||
})(AudioLevels || {});
|
|
||||||
|
|
||||||
module.exports = AudioLevels;
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Utility class for drawing canvas shapes.
|
* Utility class for drawing canvas shapes.
|
||||||
*/
|
*/
|
||||||
var CanvasUtil = (function(my) {
|
const CanvasUtil = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a round rectangle with a glow. The glowWidth indicates the depth
|
* Draws a round rectangle with a glow. The glowWidth indicates the depth
|
||||||
|
@ -15,8 +15,7 @@ var CanvasUtil = (function(my) {
|
||||||
* @param glowColor the color of the glow
|
* @param glowColor the color of the glow
|
||||||
* @param glowWidth the width of the glow
|
* @param glowWidth the width of the glow
|
||||||
*/
|
*/
|
||||||
my.drawRoundRectGlow
|
drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
|
||||||
= function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
|
|
||||||
|
|
||||||
// Save the previous state of the context.
|
// Save the previous state of the context.
|
||||||
drawContext.save();
|
drawContext.save();
|
||||||
|
@ -73,14 +72,14 @@ var CanvasUtil = (function(my) {
|
||||||
|
|
||||||
// Restore the previous context state.
|
// Restore the previous context state.
|
||||||
drawContext.restore();
|
drawContext.restore();
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the given canvas.
|
* Clones the given canvas.
|
||||||
*
|
*
|
||||||
* @return the new cloned canvas.
|
* @return the new cloned canvas.
|
||||||
*/
|
*/
|
||||||
my.cloneCanvas = function (oldCanvas) {
|
cloneCanvas (oldCanvas) {
|
||||||
/*
|
/*
|
||||||
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
|
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
|
||||||
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
|
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
|
||||||
|
@ -103,9 +102,7 @@ var CanvasUtil = (function(my) {
|
||||||
|
|
||||||
//return the new canvas
|
//return the new canvas
|
||||||
return newCanvas;
|
return newCanvas;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return my;
|
export default CanvasUtil;
|
||||||
})(CanvasUtil || {});
|
|
||||||
|
|
||||||
module.exports = CanvasUtil;
|
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
/* global JitsiMeetJS, APP */
|
||||||
|
|
||||||
|
import LoginDialog from './LoginDialog';
|
||||||
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
|
import UIUtil from '../util/UIUtil';
|
||||||
|
import {openConnection} from '../../../connection';
|
||||||
|
|
||||||
|
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||||
|
|
||||||
|
let externalAuthWindow;
|
||||||
|
let authRequiredDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate using external service or just focus
|
||||||
|
* external auth window if there is one already.
|
||||||
|
*
|
||||||
|
* @param {JitsiConference} room
|
||||||
|
* @param {string} [lockPassword] password to use if the conference is locked
|
||||||
|
*/
|
||||||
|
function doExternalAuth (room, lockPassword) {
|
||||||
|
if (externalAuthWindow) {
|
||||||
|
externalAuthWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (room.isJoined()) {
|
||||||
|
room.getExternalAuthUrl(true).then(function (url) {
|
||||||
|
externalAuthWindow = LoginDialog.showExternalAuthDialog(
|
||||||
|
url,
|
||||||
|
function () {
|
||||||
|
externalAuthWindow = null;
|
||||||
|
room.join(lockPassword);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If conference has not been started yet
|
||||||
|
// then redirect to login page
|
||||||
|
room.getExternalAuthUrl().then(UIUtil.redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate on the server.
|
||||||
|
* @param {JitsiConference} room
|
||||||
|
* @param {string} [lockPassword] password to use if the conference is locked
|
||||||
|
*/
|
||||||
|
function doXmppAuth (room, lockPassword) {
|
||||||
|
let loginDialog = LoginDialog.showAuthDialog(function (id, password) {
|
||||||
|
// auth "on the fly":
|
||||||
|
// 1. open new connection with proper id and password
|
||||||
|
// 2. connect to the room
|
||||||
|
// (this will store sessionId in the localStorage)
|
||||||
|
// 3. close new connection
|
||||||
|
// 4. reallocate focus in current room
|
||||||
|
openConnection({id, password}).then(function (connection) {
|
||||||
|
// open room
|
||||||
|
let newRoom = connection.initJitsiConference(room.getName());
|
||||||
|
|
||||||
|
loginDialog.displayConnectionStatus(
|
||||||
|
APP.translation.translateString('connection.FETCH_SESSION_ID')
|
||||||
|
);
|
||||||
|
|
||||||
|
newRoom.room.moderator.authenticate().then(function () {
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
loginDialog.displayConnectionStatus(
|
||||||
|
APP.translation.translateString('connection.GOT_SESSION_ID')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (room.isJoined()) {
|
||||||
|
// just reallocate focus if already joined
|
||||||
|
room.room.moderator.allocateConferenceFocus();
|
||||||
|
} else {
|
||||||
|
// or join
|
||||||
|
room.join(lockPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
loginDialog.close();
|
||||||
|
}).catch(function (error, code) {
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
console.error('Auth on the fly failed', error);
|
||||||
|
|
||||||
|
let errorMsg = APP.translation.translateString(
|
||||||
|
'connection.GET_SESSION_ID_ERROR'
|
||||||
|
);
|
||||||
|
|
||||||
|
loginDialog.displayError(errorMsg + code);
|
||||||
|
});
|
||||||
|
}, function (err) {
|
||||||
|
loginDialog.displayError(err);
|
||||||
|
});
|
||||||
|
}, function () { // user canceled
|
||||||
|
loginDialog.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate for the conference.
|
||||||
|
* Uses external service for auth if conference supports that.
|
||||||
|
* @param {JitsiConference} room
|
||||||
|
* @param {string} [lockPassword] password to use if the conference is locked
|
||||||
|
*/
|
||||||
|
function authenticate (room, lockPassword) {
|
||||||
|
if (room.isExternalAuthEnabled()) {
|
||||||
|
doExternalAuth(room, lockPassword);
|
||||||
|
} else {
|
||||||
|
doXmppAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify user that authentication is required to create the conference.
|
||||||
|
*/
|
||||||
|
function requireAuth(roomName) {
|
||||||
|
if (authRequiredDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authRequiredDialog = LoginDialog.showAuthRequiredDialog(
|
||||||
|
roomName, authenticate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close auth-related dialogs if there are any.
|
||||||
|
*/
|
||||||
|
function closeAuth() {
|
||||||
|
if (externalAuthWindow) {
|
||||||
|
externalAuthWindow.close();
|
||||||
|
externalAuthWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authRequiredDialog) {
|
||||||
|
authRequiredDialog.close();
|
||||||
|
authRequiredDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
authenticate,
|
||||||
|
requireAuth,
|
||||||
|
closeAuth
|
||||||
|
};
|
|
@ -1,124 +0,0 @@
|
||||||
/* global $, APP*/
|
|
||||||
|
|
||||||
var LoginDialog = require('./LoginDialog');
|
|
||||||
var Moderator = require('../../xmpp/moderator');
|
|
||||||
|
|
||||||
/* Initial "authentication required" dialog */
|
|
||||||
var authDialog = null;
|
|
||||||
/* Loop retry ID that wits for other user to create the room */
|
|
||||||
var authRetryId = null;
|
|
||||||
var authenticationWindow = null;
|
|
||||||
|
|
||||||
var Authentication = {
|
|
||||||
openAuthenticationDialog: function (roomName, intervalCallback, callback) {
|
|
||||||
// This is the loop that will wait for the room to be created by
|
|
||||||
// someone else. 'auth_required.moderator' will bring us back here.
|
|
||||||
authRetryId = window.setTimeout(intervalCallback, 5000);
|
|
||||||
// Show prompt only if it's not open
|
|
||||||
if (authDialog !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// extract room name from 'room@muc.server.net'
|
|
||||||
var room = roomName.substr(0, roomName.indexOf('@'));
|
|
||||||
|
|
||||||
var title
|
|
||||||
= APP.translation.generateTranslationHTML("dialog.WaitingForHost");
|
|
||||||
var msg
|
|
||||||
= APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.WaitForHostMsg", {room: room});
|
|
||||||
|
|
||||||
var buttonTxt
|
|
||||||
= APP.translation.generateTranslationHTML("dialog.IamHost");
|
|
||||||
var buttons = [];
|
|
||||||
buttons.push({title: buttonTxt, value: "authNow"});
|
|
||||||
|
|
||||||
authDialog = APP.UI.messageHandler.openDialog(
|
|
||||||
title,
|
|
||||||
msg,
|
|
||||||
true,
|
|
||||||
buttons,
|
|
||||||
function (onSubmitEvent, submitValue) {
|
|
||||||
|
|
||||||
// Do not close the dialog yet
|
|
||||||
onSubmitEvent.preventDefault();
|
|
||||||
|
|
||||||
// Open login popup
|
|
||||||
if (submitValue === 'authNow') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
closeAuthenticationWindow: function () {
|
|
||||||
if (authenticationWindow) {
|
|
||||||
authenticationWindow.close();
|
|
||||||
authenticationWindow = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xmppAuthenticate: function () {
|
|
||||||
|
|
||||||
var loginDialog = LoginDialog.show(
|
|
||||||
function (connection, state) {
|
|
||||||
if (!state) {
|
|
||||||
// User cancelled
|
|
||||||
loginDialog.close();
|
|
||||||
return;
|
|
||||||
} else if (state == APP.xmpp.Status.CONNECTED) {
|
|
||||||
|
|
||||||
loginDialog.close();
|
|
||||||
|
|
||||||
Authentication.stopInterval();
|
|
||||||
Authentication.closeAuthenticationDialog();
|
|
||||||
|
|
||||||
// Close the connection as anonymous one will be used
|
|
||||||
// to create the conference. Session-id will authorize
|
|
||||||
// the request.
|
|
||||||
connection.disconnect();
|
|
||||||
|
|
||||||
var roomName = APP.UI.generateRoomName();
|
|
||||||
Moderator.allocateConferenceFocus(roomName, function () {
|
|
||||||
// If it's not "on the fly" authentication now join
|
|
||||||
// the conference room
|
|
||||||
if (!APP.xmpp.isMUCJoined()) {
|
|
||||||
APP.UI.checkForNicknameAndJoin();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
},
|
|
||||||
focusAuthenticationWindow: function () {
|
|
||||||
// If auth window exists just bring it to the front
|
|
||||||
if (authenticationWindow) {
|
|
||||||
authenticationWindow.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeAuthenticationDialog: function () {
|
|
||||||
// Close authentication dialog if opened
|
|
||||||
if (authDialog) {
|
|
||||||
authDialog.close();
|
|
||||||
authDialog = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createAuthenticationWindow: function (callback, url) {
|
|
||||||
authenticationWindow = APP.UI.messageHandler.openCenteredPopup(
|
|
||||||
url, 910, 660,
|
|
||||||
// On closed
|
|
||||||
function () {
|
|
||||||
// Close authentication dialog if opened
|
|
||||||
Authentication.closeAuthenticationDialog();
|
|
||||||
callback();
|
|
||||||
authenticationWindow = null;
|
|
||||||
});
|
|
||||||
return authenticationWindow;
|
|
||||||
},
|
|
||||||
stopInterval: function () {
|
|
||||||
// Clear retry interval, so that we don't call 'doJoinAfterFocus' twice
|
|
||||||
if (authRetryId) {
|
|
||||||
window.clearTimeout(authRetryId);
|
|
||||||
authRetryId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Authentication;
|
|
|
@ -1,75 +1,102 @@
|
||||||
/* global $, APP, config*/
|
/* global $, APP, config*/
|
||||||
|
|
||||||
var XMPP = require('../../xmpp/xmpp');
|
var messageHandler = require('../util/MessageHandler');
|
||||||
var Moderator = require('../../xmpp/moderator');
|
|
||||||
|
|
||||||
//FIXME: use LoginDialog to add retries to XMPP.connect method used when
|
|
||||||
// anonymous domain is not enabled
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new <tt>Dialog</tt> instance.
|
* Build html for "password required" dialog.
|
||||||
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt> called
|
* @returns {string} html string
|
||||||
* when we either fail to connect or succeed(check Strophe.Status).
|
|
||||||
* @param obtainSession <tt>true</tt> if we want to send ConferenceIQ to Jicofo
|
|
||||||
* in order to create session-id after the connection is established.
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
function Dialog(callback, obtainSession) {
|
function getPasswordInputHtml() {
|
||||||
|
let placeholder = config.hosts.authdomain
|
||||||
|
? "user identity"
|
||||||
|
: "user@domain.net";
|
||||||
|
let passRequiredMsg = APP.translation.translateString(
|
||||||
|
"dialog.passwordRequired"
|
||||||
|
);
|
||||||
|
return `
|
||||||
|
<h2 data-i18n="dialog.passwordRequired">${passRequiredMsg}</h2>
|
||||||
|
<input name="username" type="text" placeholder=${placeholder} autofocus>
|
||||||
|
<input name="password" type="password"
|
||||||
|
data-i18n="[placeholder]dialog.userPassword"
|
||||||
|
placeholder="user password">
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
var self = this;
|
/**
|
||||||
|
* Convert provided id to jid if it's not jid yet.
|
||||||
var stop = false;
|
* @param {string} id user id or jid
|
||||||
|
* @returns {string} jid
|
||||||
var connection = APP.xmpp.createConnection();
|
*/
|
||||||
|
function toJid(id) {
|
||||||
var message = '<h2 data-i18n="dialog.passwordRequired">';
|
if (id.indexOf("@") >= 0) {
|
||||||
message += APP.translation.translateString("dialog.passwordRequired");
|
return id;
|
||||||
message += '</h2>' +
|
|
||||||
'<input name="username" type="text" ';
|
|
||||||
if (config.hosts.authdomain) {
|
|
||||||
message += 'placeholder="user identity" autofocus>';
|
|
||||||
} else {
|
|
||||||
message += 'placeholder="user@domain.net" autofocus>';
|
|
||||||
}
|
}
|
||||||
message += '<input name="password" ' +
|
|
||||||
'type="password" data-i18n="[placeholder]dialog.userPassword"' +
|
|
||||||
' placeholder="user password">';
|
|
||||||
|
|
||||||
var okButton = APP.translation.generateTranslationHTML("dialog.Ok");
|
let jid = id.concat('@');
|
||||||
|
|
||||||
var cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
|
|
||||||
|
|
||||||
var states = {
|
|
||||||
login: {
|
|
||||||
html: message,
|
|
||||||
buttons: [
|
|
||||||
{ title: okButton, value: true},
|
|
||||||
{ title: cancelButton, value: false}
|
|
||||||
],
|
|
||||||
focus: ':input:first',
|
|
||||||
submit: function (e, v, m, f) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (v) {
|
|
||||||
var jid = f.username;
|
|
||||||
var password = f.password;
|
|
||||||
if (jid && password) {
|
|
||||||
stop = false;
|
|
||||||
if (jid.indexOf("@") < 0) {
|
|
||||||
jid = jid.concat('@');
|
|
||||||
if (config.hosts.authdomain) {
|
if (config.hosts.authdomain) {
|
||||||
jid += config.hosts.authdomain;
|
jid += config.hosts.authdomain;
|
||||||
} else {
|
} else {
|
||||||
jid += config.hosts.domain;
|
jid += config.hosts.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cancel button config for the dialog.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function cancelButton() {
|
||||||
|
return {
|
||||||
|
title: APP.translation.generateTranslationHTML("dialog.Cancel"),
|
||||||
|
value: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth dialog for JitsiConnection which supports retries.
|
||||||
|
* If no cancelCallback provided then there will be
|
||||||
|
* no cancel button on the dialog.
|
||||||
|
*
|
||||||
|
* @class LoginDialog
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param {function(jid, password)} successCallback
|
||||||
|
* @param {function} [cancelCallback] callback to invoke if user canceled.
|
||||||
|
*/
|
||||||
|
function LoginDialog(successCallback, cancelCallback) {
|
||||||
|
let loginButtons = [{
|
||||||
|
title: APP.translation.generateTranslationHTML("dialog.Ok"),
|
||||||
|
value: true
|
||||||
|
}];
|
||||||
|
let finishedButtons = [{
|
||||||
|
title: APP.translation.translateString('dialog.retry'),
|
||||||
|
value: 'retry'
|
||||||
|
}];
|
||||||
|
|
||||||
|
// show "cancel" button only if cancelCallback provided
|
||||||
|
if (cancelCallback) {
|
||||||
|
loginButtons.push(cancelButton());
|
||||||
|
finishedButtons.push(cancelButton());
|
||||||
}
|
}
|
||||||
connection.reset();
|
|
||||||
|
const states = {
|
||||||
|
login: {
|
||||||
|
html: getPasswordInputHtml(),
|
||||||
|
buttons: loginButtons,
|
||||||
|
focus: ':input:first',
|
||||||
|
submit: function (e, v, m, f) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (v) {
|
||||||
|
let jid = f.username;
|
||||||
|
let password = f.password;
|
||||||
|
if (jid && password) {
|
||||||
connDialog.goToState('connecting');
|
connDialog.goToState('connecting');
|
||||||
connection.connect(jid, password, stateHandler);
|
successCallback(toJid(jid), password);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User cancelled
|
// User cancelled
|
||||||
stop = true;
|
cancelCallback();
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -82,115 +109,23 @@ function Dialog(callback, obtainSession) {
|
||||||
finished: {
|
finished: {
|
||||||
title: APP.translation.translateString('dialog.error'),
|
title: APP.translation.translateString('dialog.error'),
|
||||||
html: '<div id="errorMessage"></div>',
|
html: '<div id="errorMessage"></div>',
|
||||||
buttons: [
|
buttons: finishedButtons,
|
||||||
{
|
|
||||||
title: APP.translation.translateString('dialog.retry'),
|
|
||||||
value: 'retry'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: APP.translation.translateString('dialog.Cancel'),
|
|
||||||
value: 'cancel'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultButton: 0,
|
defaultButton: 0,
|
||||||
submit: function (e, v, m, f) {
|
submit: function (e, v, m, f) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (v === 'retry')
|
if (v === 'retry') {
|
||||||
connDialog.goToState('login');
|
connDialog.goToState('login');
|
||||||
else
|
} else {
|
||||||
callback();
|
// User cancelled
|
||||||
|
cancelCallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var connDialog
|
var connDialog = messageHandler.openDialogWithStates(
|
||||||
= APP.UI.messageHandler.openDialogWithStates(states,
|
states, { persistent: true, closeText: '' }, null
|
||||||
{ persistent: true, closeText: '' }, null);
|
);
|
||||||
|
|
||||||
var stateHandler = function (status, message) {
|
|
||||||
if (stop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var translateKey = "connection." + XMPP.getStatusString(status);
|
|
||||||
var statusStr = APP.translation.translateString(translateKey);
|
|
||||||
|
|
||||||
// Display current state
|
|
||||||
var connectionStatus =
|
|
||||||
connDialog.getState('connecting').find('#connectionStatus');
|
|
||||||
|
|
||||||
connectionStatus.text(statusStr);
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case XMPP.Status.CONNECTED:
|
|
||||||
|
|
||||||
stop = true;
|
|
||||||
if (!obtainSession) {
|
|
||||||
callback(connection, status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Obtaining session-id status
|
|
||||||
connectionStatus.text(
|
|
||||||
APP.translation.translateString(
|
|
||||||
'connection.FETCH_SESSION_ID'));
|
|
||||||
|
|
||||||
// Authenticate with Jicofo and obtain session-id
|
|
||||||
var roomName = APP.UI.generateRoomName();
|
|
||||||
|
|
||||||
// Jicofo will return new session-id when connected
|
|
||||||
// from authenticated domain
|
|
||||||
connection.sendIQ(
|
|
||||||
Moderator.createConferenceIq(roomName),
|
|
||||||
function (result) {
|
|
||||||
|
|
||||||
connectionStatus.text(
|
|
||||||
APP.translation.translateString(
|
|
||||||
'connection.GOT_SESSION_ID'));
|
|
||||||
|
|
||||||
stop = true;
|
|
||||||
|
|
||||||
// Parse session-id
|
|
||||||
Moderator.parseSessionId(result);
|
|
||||||
|
|
||||||
callback(connection, status);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Auth on the fly failed", error);
|
|
||||||
|
|
||||||
stop = true;
|
|
||||||
|
|
||||||
var errorMsg =
|
|
||||||
APP.translation.translateString(
|
|
||||||
'connection.GET_SESSION_ID_ERROR') +
|
|
||||||
$(error).find('>error').attr('code');
|
|
||||||
|
|
||||||
self.displayError(errorMsg);
|
|
||||||
|
|
||||||
connection.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
case XMPP.Status.AUTHFAIL:
|
|
||||||
case XMPP.Status.CONNFAIL:
|
|
||||||
case XMPP.Status.DISCONNECTED:
|
|
||||||
|
|
||||||
stop = true;
|
|
||||||
|
|
||||||
callback(connection, status);
|
|
||||||
|
|
||||||
var errorMessage = statusStr;
|
|
||||||
|
|
||||||
if (message)
|
|
||||||
{
|
|
||||||
errorMessage += ': ' + message;
|
|
||||||
}
|
|
||||||
self.displayError(errorMessage);
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays error message in 'finished' state which allows either to cancel
|
* Displays error message in 'finished' state which allows either to cancel
|
||||||
|
@ -199,42 +134,103 @@ function Dialog(callback, obtainSession) {
|
||||||
*/
|
*/
|
||||||
this.displayError = function (message) {
|
this.displayError = function (message) {
|
||||||
|
|
||||||
var finishedState = connDialog.getState('finished');
|
let finishedState = connDialog.getState('finished');
|
||||||
|
|
||||||
var errorMessageElem = finishedState.find('#errorMessage');
|
let errorMessageElem = finishedState.find('#errorMessage');
|
||||||
errorMessageElem.text(message);
|
errorMessageElem.text(message);
|
||||||
|
|
||||||
connDialog.goToState('finished');
|
connDialog.goToState('finished');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show message as connection status.
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
this.displayConnectionStatus = function (message) {
|
||||||
|
let connectingState = connDialog.getState('connecting');
|
||||||
|
|
||||||
|
let connectionStatus = connectingState.find('#connectionStatus');
|
||||||
|
connectionStatus.text(message);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes LoginDialog.
|
* Closes LoginDialog.
|
||||||
*/
|
*/
|
||||||
this.close = function () {
|
this.close = function () {
|
||||||
stop = true;
|
|
||||||
connDialog.close();
|
connDialog.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var LoginDialog = {
|
export default {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays login prompt used to establish new XMPP connection. Given
|
* Show new auth dialog for JitsiConnection.
|
||||||
* <tt>callback(Strophe.Connection, Strophe.Status)</tt> function will be
|
*
|
||||||
* called when we connect successfully(status === CONNECTED) or when we fail
|
* @param {function(jid, password)} successCallback
|
||||||
* to do so. On connection failure program can call Dialog.close() method in
|
* @param {function} [cancelCallback] callback to invoke if user canceled.
|
||||||
* order to cancel or do nothing to let the user retry.
|
*
|
||||||
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt>
|
* @returns {LoginDialog}
|
||||||
* called when we either fail to connect or succeed(check
|
|
||||||
* Strophe.Status).
|
|
||||||
* @param obtainSession <tt>true</tt> if we want to send ConferenceIQ to
|
|
||||||
* Jicofo in order to create session-id after the connection is
|
|
||||||
* established.
|
|
||||||
* @returns {Dialog}
|
|
||||||
*/
|
*/
|
||||||
show: function (callback, obtainSession) {
|
showAuthDialog: function (successCallback, cancelCallback) {
|
||||||
return new Dialog(callback, obtainSession);
|
return new LoginDialog(successCallback, cancelCallback);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification that external auth is required (using provided url).
|
||||||
|
* @param {string} url URL to use for external auth.
|
||||||
|
* @param {function} callback callback to invoke when auth popup is closed.
|
||||||
|
* @returns auth dialog
|
||||||
|
*/
|
||||||
|
showExternalAuthDialog: function (url, callback) {
|
||||||
|
var dialog = messageHandler.openCenteredPopup(
|
||||||
|
url, 910, 660,
|
||||||
|
// On closed
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dialog) {
|
||||||
|
messageHandler.openMessageDialog(null, "dialog.popupError");
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification that authentication is required
|
||||||
|
* to create the conference, so he should authenticate or wait for a host.
|
||||||
|
* @param {string} roomName name of the conference
|
||||||
|
* @param {function} onAuthNow callback to invoke if
|
||||||
|
* user want to authenticate.
|
||||||
|
* @returns dialog
|
||||||
|
*/
|
||||||
|
showAuthRequiredDialog: function (roomName, onAuthNow) {
|
||||||
|
var title = APP.translation.generateTranslationHTML(
|
||||||
|
"dialog.WaitingForHost"
|
||||||
|
);
|
||||||
|
var msg = APP.translation.generateTranslationHTML(
|
||||||
|
"dialog.WaitForHostMsg", {room: roomName}
|
||||||
|
);
|
||||||
|
|
||||||
|
var buttonTxt = APP.translation.generateTranslationHTML(
|
||||||
|
"dialog.IamHost"
|
||||||
|
);
|
||||||
|
var buttons = [{title: buttonTxt, value: "authNow"}];
|
||||||
|
|
||||||
|
return APP.UI.messageHandler.openDialog(
|
||||||
|
title,
|
||||||
|
msg,
|
||||||
|
true,
|
||||||
|
buttons,
|
||||||
|
function (e, submitValue) {
|
||||||
|
|
||||||
|
// Do not close the dialog yet
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Open login popup
|
||||||
|
if (submitValue === 'authNow') {
|
||||||
|
onAuthNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = LoginDialog;
|
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
/* global APP, JitsiMeetJS */
|
||||||
|
import messageHandler from '../util/MessageHandler';
|
||||||
|
import UIUtil from '../util/UIUtil';
|
||||||
|
//FIXME:
|
||||||
|
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog which asks user for new password for the conference.
|
||||||
|
* @returns {Promise<string>} password or nothing if user canceled
|
||||||
|
*/
|
||||||
|
function askForNewPassword () {
|
||||||
|
let passMsg = APP.translation.generateTranslationHTML("dialog.passwordMsg");
|
||||||
|
let yourPassMsg = APP.translation.translateString("dialog.yourPassword");
|
||||||
|
let msg = `
|
||||||
|
<h2>${passMsg}</h2>
|
||||||
|
<input name="lockKey" type="text"
|
||||||
|
data-i18n="[placeholder]dialog.yourPassword"
|
||||||
|
placeholder="${yourPassMsg}" autofocus>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
messageHandler.openTwoButtonDialog(
|
||||||
|
null, null, null,
|
||||||
|
msg, false, "dialog.Save",
|
||||||
|
function (e, v, m, f) {
|
||||||
|
if (v && f.lockKey) {
|
||||||
|
resolve(UIUtil.escapeHtml(f.lockKey));
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null, null, 'input:first'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog which asks for required conference password.
|
||||||
|
* @returns {Promise<string>} password or nothing if user canceled
|
||||||
|
*/
|
||||||
|
function askForPassword () {
|
||||||
|
let passRequiredMsg = APP.translation.translateString(
|
||||||
|
"dialog.passwordRequired"
|
||||||
|
);
|
||||||
|
let passMsg = APP.translation.translateString("dialog.password");
|
||||||
|
let msg = `
|
||||||
|
<h2 data-i18n="dialog.passwordRequired">${passRequiredMsg}</h2>
|
||||||
|
<input name="lockKey" type="text"
|
||||||
|
data-i18n="[placeholder]dialog.password"
|
||||||
|
placeholder="${passMsg}" autofocus>
|
||||||
|
`;
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
messageHandler.openTwoButtonDialog(
|
||||||
|
null, null, null, msg,
|
||||||
|
true, "dialog.Ok",
|
||||||
|
function (e, v, m, f) {}, null,
|
||||||
|
function (e, v, m, f) {
|
||||||
|
if (v && f.lockKey) {
|
||||||
|
resolve(UIUtil.escapeHtml(f.lockKey));
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
':input:first'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show dialog which asks if user want remove password from the conference.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
function askToUnlock () {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
messageHandler.openTwoButtonDialog(
|
||||||
|
null, null, "dialog.passwordCheck",
|
||||||
|
null, false, "dialog.Remove",
|
||||||
|
function (e, v) {
|
||||||
|
if (v) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification that user cannot set password for the conference
|
||||||
|
* because server doesn't support that.
|
||||||
|
*/
|
||||||
|
function notifyPasswordNotSupported () {
|
||||||
|
console.warn('room passwords not supported');
|
||||||
|
messageHandler.showError("dialog.warning", "dialog.passwordNotSupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification that setting password for the conference failed.
|
||||||
|
* @param {Error} err error
|
||||||
|
*/
|
||||||
|
function notifyPasswordFailed(err) {
|
||||||
|
console.warn('setting password failed', err);
|
||||||
|
messageHandler.showError("dialog.lockTitle", "dialog.lockMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConferenceErrors = JitsiMeetJS.errors.conference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new RoomLocker for the conference.
|
||||||
|
* It allows to set or remove password for the conference,
|
||||||
|
* or ask for required password.
|
||||||
|
* @returns {RoomLocker}
|
||||||
|
*/
|
||||||
|
export default function createRoomLocker (room) {
|
||||||
|
let password;
|
||||||
|
|
||||||
|
function lock (newPass) {
|
||||||
|
return room.lock(newPass).then(function () {
|
||||||
|
password = newPass;
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) {
|
||||||
|
notifyPasswordNotSupported();
|
||||||
|
} else {
|
||||||
|
notifyPasswordFailed(err);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class RoomLocker
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
get isLocked () {
|
||||||
|
return !!password;
|
||||||
|
},
|
||||||
|
|
||||||
|
get password () {
|
||||||
|
return password;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to remove password from the conference (asks user first).
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
askToUnlock () {
|
||||||
|
return askToUnlock().then(function () {
|
||||||
|
return lock();
|
||||||
|
}).then(function () {
|
||||||
|
AnalyticsAdapter.sendEvent('toolbar.lock.disabled');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to set password for the conference.
|
||||||
|
* It asks user for new password and locks the room.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
askToLock () {
|
||||||
|
return askForNewPassword().then(function (newPass) {
|
||||||
|
return lock(newPass);
|
||||||
|
}).then(function () {
|
||||||
|
AnalyticsAdapter.sendEvent('toolbar.lock.enabled');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks user for required conference password.
|
||||||
|
*/
|
||||||
|
requirePassword () {
|
||||||
|
return askForPassword().then(function (newPass) {
|
||||||
|
password = newPass;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification that to set/remove password user must be moderator.
|
||||||
|
*/
|
||||||
|
notifyModeratorRequired () {
|
||||||
|
if (password) {
|
||||||
|
messageHandler.openMessageDialog(null, "dialog.passwordError");
|
||||||
|
} else {
|
||||||
|
messageHandler.openMessageDialog(null, "dialog.passwordError2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,68 +1,62 @@
|
||||||
/* global Strophe, APP, MD5, config, interfaceConfig */
|
/* global MD5, config, interfaceConfig */
|
||||||
var Settings = require("../../settings/Settings");
|
|
||||||
|
|
||||||
var users = {};
|
let users = {};
|
||||||
|
|
||||||
var Avatar = {
|
export default {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the user's avatar in the settings menu(if local user), contact list
|
* Sets the user's avatar in the settings menu(if local user), contact list
|
||||||
* and thumbnail
|
* and thumbnail
|
||||||
* @param jid jid of the user
|
* @param id id of the user
|
||||||
* @param id email or userID to be used as a hash
|
* @param email email or nickname to be used as a hash
|
||||||
*/
|
*/
|
||||||
setUserAvatar: function (jid, id) {
|
setUserAvatar: function (id, email) {
|
||||||
if (id) {
|
if (email) {
|
||||||
if (users[jid] === id) {
|
if (users[id] === email) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users[jid] = id;
|
users[id] = email;
|
||||||
}
|
}
|
||||||
var avatarUrl = this.getAvatarUrl(jid);
|
|
||||||
var resourceJid = Strophe.getResourceFromJid(jid);
|
|
||||||
|
|
||||||
APP.UI.userAvatarChanged(resourceJid, avatarUrl);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL of the image for the avatar of a particular user,
|
* Returns the URL of the image for the avatar of a particular user,
|
||||||
* identified by its jid
|
* identified by its id.
|
||||||
* @param jid
|
* @param {string} userId user id
|
||||||
* @param jid full MUC jid of the user for whom we want to obtain avatar URL
|
|
||||||
*/
|
*/
|
||||||
getAvatarUrl: function (jid) {
|
getAvatarUrl: function (userId) {
|
||||||
if (config.disableThirdPartyRequests) {
|
if (config.disableThirdPartyRequests) {
|
||||||
return 'images/avatar2.png';
|
return 'images/avatar2.png';
|
||||||
} else {
|
}
|
||||||
if (!jid) {
|
|
||||||
console.error("Get avatar - jid is undefined");
|
if (!userId) {
|
||||||
|
console.error("Get avatar - id is undefined");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var id = users[jid];
|
|
||||||
|
let avatarId = users[userId];
|
||||||
|
|
||||||
// If the ID looks like an email, we'll use gravatar.
|
// If the ID looks like an email, we'll use gravatar.
|
||||||
// Otherwise, it's a random avatar, and we'll use the configured
|
// Otherwise, it's a random avatar, and we'll use the configured
|
||||||
// URL.
|
// URL.
|
||||||
var random = !id || id.indexOf('@') < 0;
|
let random = !avatarId || avatarId.indexOf('@') < 0;
|
||||||
|
|
||||||
if (!id) {
|
if (!avatarId) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"No avatar stored yet for " + jid + " - using JID as ID");
|
`No avatar stored yet for ${userId} - using ID as avatar ID`);
|
||||||
id = jid;
|
avatarId = userId;
|
||||||
}
|
}
|
||||||
id = MD5.hexdigest(id.trim().toLowerCase());
|
avatarId = MD5.hexdigest(avatarId.trim().toLowerCase());
|
||||||
|
|
||||||
// Default to using gravatar.
|
// Default to using gravatar.
|
||||||
var urlPref = 'https://www.gravatar.com/avatar/';
|
let urlPref = 'https://www.gravatar.com/avatar/';
|
||||||
var urlSuf = "?d=wavatar&size=100";
|
let urlSuf = "?d=wavatar&size=100";
|
||||||
|
|
||||||
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
|
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
|
||||||
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
|
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
|
||||||
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
|
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlPref + id + urlSuf;
|
return urlPref + avatarId + urlSuf;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Avatar;
|
|
||||||
|
|
|
@ -1,61 +1,20 @@
|
||||||
/* global $, config,
|
/* global $ */
|
||||||
setLargeVideoVisible, Util */
|
|
||||||
|
|
||||||
var VideoLayout = require("../videolayout/VideoLayout");
|
|
||||||
var Prezi = require("../prezi/Prezi");
|
|
||||||
var UIUtil = require("../util/UIUtil");
|
|
||||||
|
|
||||||
var etherpadName = null;
|
|
||||||
var etherpadIFrame = null;
|
|
||||||
var domain = null;
|
|
||||||
var options = "?showControls=true&showChat=false&showLineNumbers=true" +
|
|
||||||
"&useMonospaceFont=false";
|
|
||||||
|
|
||||||
|
import VideoLayout from "../videolayout/VideoLayout";
|
||||||
|
import LargeContainer from '../videolayout/LargeContainer';
|
||||||
|
import UIUtil from "../util/UIUtil";
|
||||||
|
import SidePanelToggler from "../side_pannels/SidePanelToggler";
|
||||||
|
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the etherpad.
|
* Etherpad options.
|
||||||
*/
|
*/
|
||||||
function resize() {
|
const options = $.param({
|
||||||
if ($('#etherpad>iframe').length) {
|
showControns: true,
|
||||||
var remoteVideos = $('#remoteVideos');
|
showChat: false,
|
||||||
var availableHeight
|
showLineNumbers: true,
|
||||||
= window.innerHeight - remoteVideos.outerHeight();
|
useMonospaceFont: false
|
||||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
});
|
||||||
|
|
||||||
$('#etherpad>iframe').width(availableWidth);
|
|
||||||
$('#etherpad>iframe').height(availableHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the Etherpad button and adds it to the toolbar.
|
|
||||||
*/
|
|
||||||
function enableEtherpadButton() {
|
|
||||||
if (!$('#toolbar_button_etherpad').is(":visible"))
|
|
||||||
$('#toolbar_button_etherpad').css({display: 'inline-block'});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the IFrame for the etherpad.
|
|
||||||
*/
|
|
||||||
function createIFrame() {
|
|
||||||
etherpadIFrame = VideoLayout.createEtherpadIframe(
|
|
||||||
domain + etherpadName + options, function() {
|
|
||||||
|
|
||||||
document.domain = document.domain;
|
|
||||||
bubbleIframeMouseMove(etherpadIFrame);
|
|
||||||
setTimeout(function() {
|
|
||||||
// the iframes inside of the etherpad are
|
|
||||||
// not yet loaded when the etherpad iframe is loaded
|
|
||||||
var outer = etherpadIFrame.
|
|
||||||
contentDocument.getElementsByName("ace_outer")[0];
|
|
||||||
bubbleIframeMouseMove(outer);
|
|
||||||
var inner = outer.
|
|
||||||
contentDocument.getElementsByName("ace_inner")[0];
|
|
||||||
bubbleIframeMouseMove(inner);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bubbleIframeMouseMove(iframe){
|
function bubbleIframeMouseMove(iframe){
|
||||||
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
||||||
|
@ -84,48 +43,140 @@ function bubbleIframeMouseMove(iframe){
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
var Etherpad = {
|
* Default Etherpad frame width.
|
||||||
/**
|
|
||||||
* Initializes the etherpad.
|
|
||||||
*/
|
*/
|
||||||
init: function (name) {
|
const DEFAULT_WIDTH = 640;
|
||||||
|
/**
|
||||||
if (config.etherpad_base && !etherpadName && name) {
|
* Default Etherpad frame height.
|
||||||
|
|
||||||
domain = config.etherpad_base;
|
|
||||||
|
|
||||||
etherpadName = name;
|
|
||||||
|
|
||||||
enableEtherpadButton();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes the etherpad, when the window is resized.
|
|
||||||
*/
|
*/
|
||||||
$(window).resize(function () {
|
const DEFAULT_HEIGHT = 480;
|
||||||
resize();
|
|
||||||
|
const EtherpadContainerType = "etherpad";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for Etherpad iframe.
|
||||||
|
*/
|
||||||
|
class Etherpad extends LargeContainer {
|
||||||
|
constructor (domain, name) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
|
||||||
|
iframe.src = domain + name + '?' + options;
|
||||||
|
iframe.frameBorder = 0;
|
||||||
|
iframe.scrolling = "no";
|
||||||
|
iframe.width = DEFAULT_WIDTH;
|
||||||
|
iframe.height = DEFAULT_HEIGHT;
|
||||||
|
iframe.setAttribute('style', 'visibility: hidden;');
|
||||||
|
|
||||||
|
this.container.appendChild(iframe);
|
||||||
|
|
||||||
|
iframe.onload = function() {
|
||||||
|
document.domain = document.domain;
|
||||||
|
bubbleIframeMouseMove(iframe);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
const doc = iframe.contentDocument;
|
||||||
|
|
||||||
|
// the iframes inside of the etherpad are
|
||||||
|
// not yet loaded when the etherpad iframe is loaded
|
||||||
|
const outer = doc.getElementsByName("ace_outer")[0];
|
||||||
|
bubbleIframeMouseMove(outer);
|
||||||
|
|
||||||
|
const inner = doc.getElementsByName("ace_inner")[0];
|
||||||
|
bubbleIframeMouseMove(inner);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.iframe = iframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen () {
|
||||||
|
return !!this.iframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
get container () {
|
||||||
|
return document.getElementById('etherpad');
|
||||||
|
}
|
||||||
|
|
||||||
|
resize (containerWidth, containerHeight, animate) {
|
||||||
|
let height = containerHeight - BottomToolbar.getFilmStripHeight();
|
||||||
|
let width = containerWidth;
|
||||||
|
|
||||||
|
$(this.iframe).width(width).height(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
show () {
|
||||||
|
const $iframe = $(this.iframe);
|
||||||
|
const $container = $(this.container);
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
$iframe.fadeIn(300, function () {
|
||||||
|
document.body.style.background = '#eeeeee';
|
||||||
|
$iframe.css({visibility: 'visible'});
|
||||||
|
$container.css({zIndex: 2});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
hide () {
|
||||||
|
const $iframe = $(this.iframe);
|
||||||
|
const $container = $(this.container);
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
$iframe.fadeOut(300, function () {
|
||||||
|
$iframe.css({visibility: 'hidden'});
|
||||||
|
$container.css({zIndex: 0});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager of the Etherpad frame.
|
||||||
|
*/
|
||||||
|
export default class EtherpadManager {
|
||||||
|
constructor (domain, name) {
|
||||||
|
if (!domain || !name) {
|
||||||
|
throw new Error("missing domain or name");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.domain = domain;
|
||||||
|
this.name = name;
|
||||||
|
this.etherpad = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen () {
|
||||||
|
return !!this.etherpad;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens/hides the Etherpad.
|
* Create new Etherpad frame.
|
||||||
*/
|
*/
|
||||||
toggleEtherpad: function (isPresentation) {
|
openEtherpad () {
|
||||||
if (!etherpadIFrame)
|
this.etherpad = new Etherpad(this.domain, this.name);
|
||||||
createIFrame();
|
VideoLayout.addLargeVideoContainer(
|
||||||
|
EtherpadContainerType,
|
||||||
|
this.etherpad
|
||||||
if(VideoLayout.getLargeVideoState() === "etherpad")
|
);
|
||||||
{
|
|
||||||
VideoLayout.setLargeVideoState("video");
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
VideoLayout.setLargeVideoState("etherpad");
|
|
||||||
}
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Etherpad;
|
/**
|
||||||
|
* Toggle Etherpad frame visibility.
|
||||||
|
* Open new Etherpad frame if there is no Etherpad frame yet.
|
||||||
|
*/
|
||||||
|
toggleEtherpad () {
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.openEtherpad();
|
||||||
|
}
|
||||||
|
|
||||||
|
let isVisible = VideoLayout.isLargeContainerTypeVisible(
|
||||||
|
EtherpadContainerType
|
||||||
|
);
|
||||||
|
|
||||||
|
VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,343 +1,448 @@
|
||||||
var ToolbarToggler = require("../toolbars/ToolbarToggler");
|
/* global $, APP */
|
||||||
var UIUtil = require("../util/UIUtil");
|
/* jshint -W101 */
|
||||||
var VideoLayout = require("../videolayout/VideoLayout");
|
|
||||||
var messageHandler = require("../util/MessageHandler");
|
|
||||||
var PreziPlayer = require("./PreziPlayer");
|
|
||||||
|
|
||||||
var preziPlayer = null;
|
|
||||||
|
|
||||||
|
import VideoLayout from "../videolayout/VideoLayout";
|
||||||
|
import LargeContainer from '../videolayout/LargeContainer';
|
||||||
|
import PreziPlayer from './PreziPlayer';
|
||||||
|
import UIUtil from '../util/UIUtil';
|
||||||
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
|
import messageHandler from '../util/MessageHandler';
|
||||||
|
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||||
|
import SidePanelToggler from "../side_pannels/SidePanelToggler";
|
||||||
|
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows/hides a presentation.
|
* Example of Prezi link.
|
||||||
*/
|
*/
|
||||||
function setPresentationVisible(visible) {
|
const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi";
|
||||||
|
const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i;
|
||||||
|
/**
|
||||||
|
* Default aspect ratio for Prezi frame.
|
||||||
|
*/
|
||||||
|
const aspectRatio = 16.0 / 9.0;
|
||||||
|
|
||||||
if (visible) {
|
/**
|
||||||
VideoLayout.setLargeVideoState("prezi");
|
* Default Prezi frame width.
|
||||||
}
|
*/
|
||||||
else {
|
const DEFAULT_WIDTH = 640;
|
||||||
VideoLayout.setLargeVideoState("video");
|
/**
|
||||||
}
|
* Default Prezi frame height.
|
||||||
|
*/
|
||||||
|
const DEFAULT_HEIGHT = 480;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the given string is an alphanumeric string.
|
||||||
|
* Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
|
||||||
|
* purpose of checking URIs.
|
||||||
|
* @param {string} unsafeText string to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isAlphanumeric(unsafeText) {
|
||||||
|
return alphanumRegex.test(unsafeText);
|
||||||
}
|
}
|
||||||
|
|
||||||
var Prezi = {
|
/**
|
||||||
|
* Returns the presentation id from the given url.
|
||||||
|
* @param {string} url Prezi link
|
||||||
/**
|
* @returns {string} presentation id
|
||||||
* Reloads the current presentation.
|
|
||||||
*/
|
*/
|
||||||
reloadPresentation: function() {
|
function getPresentationId (url) {
|
||||||
var iframe = document.getElementById(preziPlayer.options.preziId);
|
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||||
iframe.src = iframe.src;
|
return presId.substring(0, presId.indexOf('/'));
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns <tt>true</tt> if the presentation is visible, <tt>false</tt> -
|
* Checks if given string is Prezi url.
|
||||||
* otherwise.
|
* @param {string} url string to check.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isPresentationVisible: function () {
|
function isPreziLink(url) {
|
||||||
return ($('#presentation>iframe') != null
|
if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
|
||||||
&& $('#presentation>iframe').css('opacity') == 1);
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||||
* Opens the Prezi dialog, from which the user could choose a presentation
|
if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
|
||||||
* to load.
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify user that other user if already sharing Prezi.
|
||||||
*/
|
*/
|
||||||
openPreziDialog: function() {
|
function notifyOtherIsSharingPrezi() {
|
||||||
var myprezi = APP.xmpp.getPrezi();
|
messageHandler.openMessageDialog(
|
||||||
if (myprezi) {
|
"dialog.sharePreziTitle",
|
||||||
messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
|
"dialog.sharePreziMsg"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask user if he want to close Prezi he's sharing.
|
||||||
|
*/
|
||||||
|
function proposeToClosePrezi() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
messageHandler.openTwoButtonDialog(
|
||||||
|
"dialog.removePreziTitle",
|
||||||
null,
|
null,
|
||||||
"dialog.removePreziMsg",
|
"dialog.removePreziMsg",
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
"dialog.Remove",
|
"dialog.Remove",
|
||||||
function(e,v,m,f) {
|
function(e,v,m,f) {
|
||||||
if(v) {
|
if (v) {
|
||||||
APP.xmpp.removePreziFromPresence();
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
else if (preziPlayer != null) {
|
});
|
||||||
messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
|
}
|
||||||
null, "dialog.sharePreziMsg",
|
|
||||||
null,
|
/**
|
||||||
false,
|
* Ask user for Prezi url to share with others.
|
||||||
"dialog.Ok",
|
* Dialog validates client input to allow only Prezi urls.
|
||||||
function(e,v,m,f) {
|
*/
|
||||||
$.prompt.close();
|
function requestPreziLink() {
|
||||||
}
|
const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle");
|
||||||
|
const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
|
||||||
|
const shareButton = APP.translation.generateTranslationHTML("dialog.Share");
|
||||||
|
const backButton = APP.translation.generateTranslationHTML("dialog.Back");
|
||||||
|
const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError");
|
||||||
|
const i18nOptions = {url: defaultPreziLink};
|
||||||
|
const defaultUrl = APP.translation.translateString(
|
||||||
|
"defaultPreziLink", i18nOptions
|
||||||
);
|
);
|
||||||
}
|
|
||||||
else {
|
return new Promise(function (resolve, reject) {
|
||||||
var html = APP.translation.generateTranslationHTML(
|
let dialog = messageHandler.openDialogWithStates({
|
||||||
"dialog.sharePreziTitle");
|
|
||||||
var cancelButton = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.Cancel");
|
|
||||||
var shareButton = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.Share");
|
|
||||||
var backButton = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.Back");
|
|
||||||
var buttons = [];
|
|
||||||
var buttons1 = [];
|
|
||||||
// Cancel button to both states
|
|
||||||
buttons.push({title: cancelButton, value: false});
|
|
||||||
buttons1.push({title: cancelButton, value: false});
|
|
||||||
// Share button
|
|
||||||
buttons.push({title: shareButton, value: true});
|
|
||||||
// Back button
|
|
||||||
buttons1.push({title: backButton, value: true});
|
|
||||||
var linkError = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.preziLinkError");
|
|
||||||
var defaultUrl = APP.translation.translateString("defaultPreziLink",
|
|
||||||
{url: "http://prezi.com/wz7vhjycl7e6/my-prezi"});
|
|
||||||
var openPreziState = {
|
|
||||||
state0: {
|
state0: {
|
||||||
html: '<h2>' + html + '</h2>' +
|
html: `
|
||||||
'<input name="preziUrl" type="text" ' +
|
<h2>${title}</h2>
|
||||||
'data-i18n="[placeholder]defaultPreziLink" data-i18n-options=\'' +
|
<input name="preziUrl" type="text"
|
||||||
JSON.stringify({"url": "http://prezi.com/wz7vhjycl7e6/my-prezi"}) +
|
data-i18n="[placeholder]defaultPreziLink"
|
||||||
'\' placeholder="' + defaultUrl + '" autofocus>',
|
data-i18n-options="${JSON.stringify(i18nOptions)}"
|
||||||
|
placeholder="${defaultUrl}" autofocus>`,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
buttons: buttons,
|
buttons: [
|
||||||
focus: ':input:first',
|
{title: cancelButton, value: false},
|
||||||
defaultButton: 0,
|
{title: shareButton, value: true}
|
||||||
submit: function (e, v, m, f) {
|
],
|
||||||
e.preventDefault();
|
|
||||||
if(v)
|
|
||||||
{
|
|
||||||
var preziUrl = f.preziUrl;
|
|
||||||
|
|
||||||
if (preziUrl)
|
|
||||||
{
|
|
||||||
var urlValue
|
|
||||||
= encodeURI(UIUtil.escapeHtml(preziUrl));
|
|
||||||
|
|
||||||
if (urlValue.indexOf('http://prezi.com/') != 0
|
|
||||||
&& urlValue.indexOf('https://prezi.com/') != 0)
|
|
||||||
{
|
|
||||||
$.prompt.goToState('state1');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var presIdTmp = urlValue.substring(
|
|
||||||
urlValue.indexOf("prezi.com/") + 10);
|
|
||||||
if (!isAlphanumeric(presIdTmp)
|
|
||||||
|| presIdTmp.indexOf('/') < 2) {
|
|
||||||
$.prompt.goToState('state1');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
APP.xmpp.addToPresence("prezi", urlValue);
|
|
||||||
$.prompt.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$.prompt.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state1: {
|
|
||||||
html: '<h2>' + html + '</h2>' +
|
|
||||||
linkError,
|
|
||||||
persistent: false,
|
|
||||||
buttons: buttons1,
|
|
||||||
focus: ':input:first',
|
focus: ':input:first',
|
||||||
defaultButton: 1,
|
defaultButton: 1,
|
||||||
submit: function (e, v, m, f) {
|
submit: function (e, v, m, f) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (v === 0)
|
if (!v) {
|
||||||
$.prompt.close();
|
reject('cancelled');
|
||||||
else
|
dialog.close();
|
||||||
$.prompt.goToState('state0');
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
messageHandler.openDialogWithStates(openPreziState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
let preziUrl = f.preziUrl;
|
||||||
|
if (!preziUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl));
|
||||||
* A new presentation has been added.
|
|
||||||
*
|
|
||||||
* @param event the event indicating the add of a presentation
|
|
||||||
* @param jid the jid from which the presentation was added
|
|
||||||
* @param presUrl url of the presentation
|
|
||||||
* @param currentSlide the current slide to which we should move
|
|
||||||
*/
|
|
||||||
function presentationAdded(event, jid, presUrl, currentSlide) {
|
|
||||||
console.log("presentation added", presUrl);
|
|
||||||
|
|
||||||
var presId = getPresentationId(presUrl);
|
if (!isPreziLink(urlValue)) {
|
||||||
|
dialog.goToState('state1');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var elementId = 'participant_'
|
resolve(urlValue);
|
||||||
+ Strophe.getResourceFromJid(jid)
|
dialog.close();
|
||||||
+ '_' + presId;
|
|
||||||
|
|
||||||
VideoLayout.addPreziContainer(elementId);
|
|
||||||
|
|
||||||
var controlsEnabled = false;
|
|
||||||
if (jid === APP.xmpp.myJid())
|
|
||||||
controlsEnabled = true;
|
|
||||||
|
|
||||||
setPresentationVisible(true);
|
|
||||||
VideoLayout.setLargeVideoHover(
|
|
||||||
function (event) {
|
|
||||||
if (Prezi.isPresentationVisible()) {
|
|
||||||
var reloadButtonRight = window.innerWidth
|
|
||||||
- $('#presentation>iframe').offset().left
|
|
||||||
- $('#presentation>iframe').width();
|
|
||||||
|
|
||||||
$('#reloadPresentation').css({ right: reloadButtonRight,
|
|
||||||
display:'inline-block'});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (event) {
|
|
||||||
if (!Prezi.isPresentationVisible())
|
|
||||||
$('#reloadPresentation').css({display:'none'});
|
|
||||||
else {
|
|
||||||
var e = event.toElement || event.relatedTarget;
|
|
||||||
|
|
||||||
if (e && e.id != 'reloadPresentation' && e.id != 'header')
|
state1: {
|
||||||
$('#reloadPresentation').css({display:'none'});
|
html: `<h2>${title}</h2> ${linkError}`,
|
||||||
|
persistent: false,
|
||||||
|
buttons: [
|
||||||
|
{title: cancelButton, value: false},
|
||||||
|
{title: backButton, value: true}
|
||||||
|
],
|
||||||
|
focus: ':input:first',
|
||||||
|
defaultButton: 1,
|
||||||
|
submit: function (e, v, m, f) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (v === 0) {
|
||||||
|
reject();
|
||||||
|
dialog.close();
|
||||||
|
} else {
|
||||||
|
dialog.goToState('state0');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
preziPlayer = new PreziPlayer(
|
});
|
||||||
'presentation',
|
}
|
||||||
{preziId: presId,
|
|
||||||
width: getPresentationWidth(),
|
export const PreziContainerType = "prezi";
|
||||||
height: getPresentationHeihgt(),
|
|
||||||
controls: controlsEnabled,
|
/**
|
||||||
|
* Container for Prezi iframe.
|
||||||
|
*/
|
||||||
|
class PreziContainer extends LargeContainer {
|
||||||
|
|
||||||
|
constructor ({preziId, isMy, slide, onSlideChanged}) {
|
||||||
|
super();
|
||||||
|
this.reloadBtn = $('#reloadPresentation');
|
||||||
|
|
||||||
|
let preziPlayer = new PreziPlayer(
|
||||||
|
'presentation', {
|
||||||
|
preziId,
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
controls: isMy,
|
||||||
debug: true
|
debug: true
|
||||||
});
|
|
||||||
|
|
||||||
$('#presentation>iframe').attr('id', preziPlayer.options.preziId);
|
|
||||||
|
|
||||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
|
|
||||||
console.log("prezi status", event.value);
|
|
||||||
if (event.value == PreziPlayer.STATUS_CONTENT_READY) {
|
|
||||||
if (jid != APP.xmpp.myJid())
|
|
||||||
preziPlayer.flyToStep(currentSlide);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) {
|
|
||||||
console.log("event value", event.value);
|
|
||||||
APP.xmpp.addToPresence("preziSlide", event.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#" + elementId).css( 'background-image',
|
|
||||||
'url(../images/avatarprezi.png)');
|
|
||||||
$("#" + elementId).click(
|
|
||||||
function () {
|
|
||||||
setPresentationVisible(true);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
this.preziPlayer = preziPlayer;
|
||||||
|
this.$iframe = $(preziPlayer.iframe);
|
||||||
|
|
||||||
/**
|
this.$iframe.attr('id', preziId);
|
||||||
* A presentation has been removed.
|
|
||||||
*
|
preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) {
|
||||||
* @param event the event indicating the remove of a presentation
|
console.log("prezi status", value);
|
||||||
* @param jid the jid for which the presentation was removed
|
if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) {
|
||||||
* @param the url of the presentation
|
preziPlayer.flyToStep(slide);
|
||||||
*/
|
|
||||||
function presentationRemoved(event, jid, presUrl) {
|
|
||||||
console.log('presentation removed', presUrl);
|
|
||||||
var presId = getPresentationId(presUrl);
|
|
||||||
setPresentationVisible(false);
|
|
||||||
$('#participant_'
|
|
||||||
+ Strophe.getResourceFromJid(jid)
|
|
||||||
+ '_' + presId).remove();
|
|
||||||
$('#presentation>iframe').remove();
|
|
||||||
if (preziPlayer != null) {
|
|
||||||
preziPlayer.destroy();
|
|
||||||
preziPlayer = null;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) {
|
||||||
* Indicates if the given string is an alphanumeric string.
|
console.log("event value", value);
|
||||||
* Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
|
onSlideChanged(value);
|
||||||
* purpose of checking URIs.
|
});
|
||||||
*/
|
|
||||||
function isAlphanumeric(unsafeText) {
|
|
||||||
var regex = /^[a-z0-9-_\/&\?=;]+$/i;
|
|
||||||
return regex.test(unsafeText);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the presentation id from the given url.
|
|
||||||
*/
|
|
||||||
function getPresentationId (presUrl) {
|
|
||||||
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
|
|
||||||
return presIdTmp.substring(0, presIdTmp.indexOf('/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the presentation width.
|
|
||||||
*/
|
|
||||||
function getPresentationWidth() {
|
|
||||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
|
||||||
var availableHeight = getPresentationHeihgt();
|
|
||||||
|
|
||||||
var aspectRatio = 16.0 / 9.0;
|
|
||||||
if (availableHeight < availableWidth / aspectRatio) {
|
|
||||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
|
||||||
}
|
}
|
||||||
return availableWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the presentation height.
|
* Change Prezi slide.
|
||||||
|
* @param {number} slide slide to show
|
||||||
*/
|
*/
|
||||||
function getPresentationHeihgt() {
|
goToSlide (slide) {
|
||||||
var remoteVideos = $('#remoteVideos');
|
if (this.preziPlayer.getCurrentStep() === slide) {
|
||||||
return window.innerHeight - remoteVideos.outerHeight();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
this.preziPlayer.flyToStep(slide);
|
||||||
* Resizes the presentation iframe.
|
|
||||||
|
let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps();
|
||||||
|
if (!animationStepsArray) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) {
|
||||||
|
this.preziPlayer.flyToStep(slide, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide "reload presentation" button.
|
||||||
|
* @param {boolean} show
|
||||||
*/
|
*/
|
||||||
function resize() {
|
showReloadBtn (show) {
|
||||||
if ($('#presentation>iframe')) {
|
this.reloadBtn.css('display', show ? 'inline-block' : 'none');
|
||||||
$('#presentation>iframe').width(getPresentationWidth());
|
}
|
||||||
$('#presentation>iframe').height(getPresentationHeihgt());
|
|
||||||
|
show () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.$iframe.fadeIn(300, () => {
|
||||||
|
this.$iframe.css({opacity: 1});
|
||||||
|
ToolbarToggler.dockToolbar(true);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hide () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.$iframe.fadeOut(300, () => {
|
||||||
|
this.$iframe.css({opacity: 0});
|
||||||
|
this.showReloadBtn(false);
|
||||||
|
ToolbarToggler.dockToolbar(false);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverIn () {
|
||||||
|
let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width();
|
||||||
|
|
||||||
|
this.showReloadBtn(true);
|
||||||
|
this.reloadBtn.css('right', rightOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverOut (event) {
|
||||||
|
let e = event.toElement || event.relatedTarget;
|
||||||
|
|
||||||
|
if (e && e.id != 'reloadPresentation' && e.id != 'header') {
|
||||||
|
this.showReloadBtn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize (containerWidth, containerHeight) {
|
||||||
|
let height = containerHeight - BottomToolbar.getFilmStripHeight();
|
||||||
|
|
||||||
|
let width = containerWidth;
|
||||||
|
|
||||||
|
if (height < width / aspectRatio) {
|
||||||
|
width = Math.floor(height * aspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$iframe.width(width).height(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close Prezi frame.
|
||||||
|
*/
|
||||||
|
close () {
|
||||||
|
this.showReloadBtn(false);
|
||||||
|
this.preziPlayer.destroy();
|
||||||
|
this.$iframe.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation has been removed.
|
* Manager of Prezi frames.
|
||||||
*/
|
*/
|
||||||
$(document).bind('presentationremoved.muc', presentationRemoved);
|
export default class PreziManager {
|
||||||
|
constructor (emitter) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
|
||||||
/**
|
this.userId = null;
|
||||||
* Presentation has been added.
|
this.url = null;
|
||||||
|
this.prezi = null;
|
||||||
|
|
||||||
|
$("#reloadPresentationLink").click(this.reloadPresentation.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get isPresenting () {
|
||||||
|
return !!this.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMyPrezi () {
|
||||||
|
return this.userId === APP.conference.localId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is currently sharing.
|
||||||
|
* @param {string} id user id to check for
|
||||||
*/
|
*/
|
||||||
$(document).bind('presentationadded.muc', presentationAdded);
|
isSharing (id) {
|
||||||
|
return this.userId === id;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
handlePreziButtonClicked () {
|
||||||
* Indicates presentation slide change.
|
if (!this.isPresenting) {
|
||||||
*/
|
requestPreziLink().then(
|
||||||
$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
|
url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0),
|
||||||
if (preziPlayer && preziPlayer.getCurrentStep() != current) {
|
err => console.error('PREZI CANCELED', err)
|
||||||
preziPlayer.flyToStep(current);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
|
if (this.isMyPrezi) {
|
||||||
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
|
proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI));
|
||||||
preziPlayer.flyToStep(current, i);
|
} else {
|
||||||
|
notifyOtherIsSharingPrezi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$(window).resize(function () {
|
/**
|
||||||
resize();
|
* Reload current Prezi frame.
|
||||||
});
|
*/
|
||||||
|
reloadPresentation () {
|
||||||
|
if (!this.prezi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let iframe = this.prezi.$iframe[0];
|
||||||
|
iframe.src = iframe.src;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = Prezi;
|
/**
|
||||||
|
* Show Prezi. Create new Prezi if there is no Prezi yet.
|
||||||
|
* @param {string} id owner id
|
||||||
|
* @param {string} url Prezi url
|
||||||
|
* @param {number} slide slide to show
|
||||||
|
*/
|
||||||
|
showPrezi (id, url, slide) {
|
||||||
|
if (!this.isPresenting) {
|
||||||
|
this.createPrezi(id, url, slide);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.userId === id && this.url === url) {
|
||||||
|
this.prezi.goToSlide(slide);
|
||||||
|
} else {
|
||||||
|
console.error(this.userId, id);
|
||||||
|
console.error(this.url, url);
|
||||||
|
throw new Error("unexpected presentation change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new Prezi frame..
|
||||||
|
* @param {string} id owner id
|
||||||
|
* @param {string} url Prezi url
|
||||||
|
* @param {number} slide slide to show
|
||||||
|
*/
|
||||||
|
createPrezi (id, url, slide) {
|
||||||
|
console.log("presentation added", url);
|
||||||
|
|
||||||
|
this.userId = id;
|
||||||
|
this.url = url;
|
||||||
|
|
||||||
|
let preziId = getPresentationId(url);
|
||||||
|
let elementId = `participant_${id}_${preziId}`;
|
||||||
|
|
||||||
|
this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId));
|
||||||
|
VideoLayout.resizeThumbnails();
|
||||||
|
this.$thumb.css({
|
||||||
|
'background-image': 'url(../images/avatarprezi.png)'
|
||||||
|
}).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true));
|
||||||
|
|
||||||
|
this.prezi = new PreziContainer({
|
||||||
|
preziId,
|
||||||
|
isMy: this.isMyPrezi,
|
||||||
|
slide,
|
||||||
|
onSlideChanged: newSlide => {
|
||||||
|
if (this.isMyPrezi) {
|
||||||
|
this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi);
|
||||||
|
VideoLayout.showLargeVideoContainer(PreziContainerType, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close Prezi.
|
||||||
|
* @param {string} id owner id
|
||||||
|
*/
|
||||||
|
removePrezi (id) {
|
||||||
|
if (this.userId !== id) {
|
||||||
|
throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$thumb.remove();
|
||||||
|
this.$thumb = null;
|
||||||
|
|
||||||
|
// wait until Prezi is hidden, then remove it
|
||||||
|
VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => {
|
||||||
|
console.log("presentation removed", this.url);
|
||||||
|
|
||||||
|
VideoLayout.removeLargeVideoContainer(PreziContainerType);
|
||||||
|
|
||||||
|
this.userId = null;
|
||||||
|
this.url = null;
|
||||||
|
this.prezi.close();
|
||||||
|
this.prezi = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,37 +1,33 @@
|
||||||
/* global PreziPlayer */
|
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
(function() {
|
|
||||||
"use strict";
|
|
||||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
|
||||||
|
|
||||||
window.PreziPlayer = (function() {
|
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||||
|
|
||||||
PreziPlayer.API_VERSION = 1;
|
PreziPlayer.API_VERSION = 1;
|
||||||
PreziPlayer.CURRENT_STEP = 'currentStep';
|
PreziPlayer.CURRENT_STEP = 'currentStep';
|
||||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||||
PreziPlayer.STATUS_LOADING = 'loading';
|
PreziPlayer.STATUS_LOADING = 'loading';
|
||||||
PreziPlayer.STATUS_READY = 'ready';
|
PreziPlayer.STATUS_READY = 'ready';
|
||||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||||
PreziPlayer.domain = "https://prezi.com";
|
PreziPlayer.domain = "https://prezi.com";
|
||||||
PreziPlayer.path = "/player/";
|
PreziPlayer.path = "/player/";
|
||||||
PreziPlayer.players = {};
|
PreziPlayer.players = {};
|
||||||
PreziPlayer.binded_methods = ['changesHandler'];
|
PreziPlayer.binded_methods = ['changesHandler'];
|
||||||
|
|
||||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||||
for(var i=0; i<optionArray.length; i++) {
|
for(var i=0; i<optionArray.length; i++) {
|
||||||
var optionSet = optionArray[i];
|
var optionSet = optionArray[i];
|
||||||
new PreziPlayer(optionSet.id, optionSet);
|
new PreziPlayer(optionSet.id, optionSet);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.messageReceived = function(event){
|
PreziPlayer.messageReceived = function(event){
|
||||||
var message, item, player;
|
var message, item, player;
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(event.data);
|
message = JSON.parse(event.data);
|
||||||
|
@ -51,11 +47,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
/*jshint -W004 */
|
/*jshint -W004 */
|
||||||
function PreziPlayer(id, options) {
|
function PreziPlayer(id, options) {
|
||||||
/*jshint +W004 */
|
/*jshint +W004 */
|
||||||
var params, paramString = "", _this = this;
|
var params, paramString = "", _this = this;
|
||||||
if (PreziPlayer.players[id]){
|
if (PreziPlayer.players[id]){
|
||||||
PreziPlayer.players[id].destroy();
|
PreziPlayer.players[id].destroy();
|
||||||
|
@ -106,9 +102,9 @@
|
||||||
_this.sendMessage({'action': 'init'});
|
_this.sendMessage({'action': 'init'});
|
||||||
}, 500);
|
}, 500);
|
||||||
PreziPlayer.players[id] = this;
|
PreziPlayer.players[id] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
PreziPlayer.prototype.changesHandler = function(message) {
|
PreziPlayer.prototype.changesHandler = function(message) {
|
||||||
var key, value, j, item;
|
var key, value, j, item;
|
||||||
if (this.initPollInterval) {
|
if (this.initPollInterval) {
|
||||||
clearInterval(this.initPollInterval);
|
clearInterval(this.initPollInterval);
|
||||||
|
@ -126,43 +122,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.destroy = function() {
|
PreziPlayer.prototype.destroy = function() {
|
||||||
if (this.initPollInterval) {
|
if (this.initPollInterval) {
|
||||||
clearInterval(this.initPollInterval);
|
clearInterval(this.initPollInterval);
|
||||||
this.initPollInterval = false;
|
this.initPollInterval = false;
|
||||||
}
|
}
|
||||||
this.embedTo.innerHTML = '';
|
this.embedTo.innerHTML = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.sendMessage = function(message) {
|
PreziPlayer.prototype.sendMessage = function(message) {
|
||||||
if (this.options.debug === true) {
|
if (this.options.debug === true) {
|
||||||
if (console && console.log) console.log('sent', message);
|
if (console && console.log) console.log('sent', message);
|
||||||
}
|
}
|
||||||
message.version = PreziPlayer.API_VERSION;
|
message.version = PreziPlayer.API_VERSION;
|
||||||
message.id = this.id;
|
message.id = this.id;
|
||||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||||
PreziPlayer.prototype.flyToNextStep = function() {
|
PreziPlayer.prototype.flyToNextStep = function() {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['moveToNextStep']
|
'data': ['moveToNextStep']
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['moveToPrevStep']
|
'data': ['moveToPrevStep']
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||||
var obj = this;
|
var obj = this;
|
||||||
// check animation_step
|
// check animation_step
|
||||||
if (animation_step > 0 &&
|
if (animation_step > 0 &&
|
||||||
|
@ -186,90 +182,90 @@
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['moveToStep', step]
|
'data': ['moveToStep', step]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['moveToObject', objectId]
|
'data': ['moveToObject', objectId]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['startAutoPlay', defaultDelay]
|
'data': ['startAutoPlay', defaultDelay]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.stop = function() {
|
PreziPlayer.prototype.stop = function() {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['stopAutoPlay']
|
'data': ['stopAutoPlay']
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||||
return this.sendMessage({
|
return this.sendMessage({
|
||||||
'action': 'present',
|
'action': 'present',
|
||||||
'data': ['pauseAutoPlay', defaultDelay]
|
'data': ['pauseAutoPlay', defaultDelay]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getCurrentStep = function() {
|
PreziPlayer.prototype.getCurrentStep = function() {
|
||||||
return this.values.currentStep;
|
return this.values.currentStep;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||||
return this.values.currentAnimationStep;
|
return this.values.currentAnimationStep;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getCurrentObject = function() {
|
PreziPlayer.prototype.getCurrentObject = function() {
|
||||||
return this.values.currentObject;
|
return this.values.currentObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getStatus = function() {
|
PreziPlayer.prototype.getStatus = function() {
|
||||||
return this.values.status;
|
return this.values.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.isPlaying = function() {
|
PreziPlayer.prototype.isPlaying = function() {
|
||||||
return this.values.isAutoPlaying;
|
return this.values.isAutoPlaying;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getStepCount = function() {
|
PreziPlayer.prototype.getStepCount = function() {
|
||||||
return this.values.stepCount;
|
return this.values.stepCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||||
return this.values.animationCountOnSteps;
|
return this.values.animationCountOnSteps;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getTitle = function() {
|
PreziPlayer.prototype.getTitle = function() {
|
||||||
return this.values.title;
|
return this.values.title;
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||||
for (var parameter in dims) {
|
for (var parameter in dims) {
|
||||||
this.iframe[parameter] = dims[parameter];
|
this.iframe[parameter] = dims[parameter];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.getDimensions = function() {
|
PreziPlayer.prototype.getDimensions = function() {
|
||||||
return {
|
return {
|
||||||
width: parseInt(this.iframe.width, 10),
|
width: parseInt(this.iframe.width, 10),
|
||||||
height: parseInt(this.iframe.height, 10)
|
height: parseInt(this.iframe.height, 10)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.on = function(event, callback) {
|
PreziPlayer.prototype.on = function(event, callback) {
|
||||||
this.callbacks.push({
|
this.callbacks.push({
|
||||||
event: event,
|
event: event,
|
||||||
callback: callback
|
callback: callback
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
PreziPlayer.prototype.off = function(event, callback) {
|
PreziPlayer.prototype.off = function(event, callback) {
|
||||||
var j, item;
|
var j, item;
|
||||||
if (event === undefined) {
|
if (event === undefined) {
|
||||||
this.callbacks = [];
|
this.callbacks = [];
|
||||||
|
@ -281,18 +277,14 @@
|
||||||
this.callbacks.splice(j, 1);
|
this.callbacks.splice(j, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.addEventListener) {
|
if (window.addEventListener) {
|
||||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||||
} else {
|
} else {
|
||||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreziPlayer;
|
window.PreziPlayer = PreziPlayer;
|
||||||
|
|
||||||
})();
|
export default PreziPlayer;
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
module.exports = PreziPlayer;
|
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
/* global require, $ */
|
/* global require, $ */
|
||||||
var Chat = require("./chat/Chat");
|
import Chat from "./chat/Chat";
|
||||||
var ContactList = require("./contactlist/ContactList");
|
import ContactList from "./contactlist/ContactList";
|
||||||
var Settings = require("./../../settings/Settings");
|
import Settings from "./../../settings/Settings";
|
||||||
var SettingsMenu = require("./settings/SettingsMenu");
|
import SettingsMenu from "./settings/SettingsMenu";
|
||||||
var VideoLayout = require("../videolayout/VideoLayout");
|
import VideoLayout from "../videolayout/VideoLayout";
|
||||||
var ToolbarToggler = require("../toolbars/ToolbarToggler");
|
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||||
var UIUtil = require("../util/UIUtil");
|
import UIUtil from "../util/UIUtil";
|
||||||
var LargeVideo = require("../videolayout/LargeVideo");
|
|
||||||
|
|
||||||
/**
|
const buttons = {
|
||||||
* Toggler for the chat, contact list, settings menu, etc..
|
|
||||||
*/
|
|
||||||
var PanelToggler = (function(my) {
|
|
||||||
|
|
||||||
var currentlyOpen = null;
|
|
||||||
var buttons = {
|
|
||||||
'#chatspace': '#chatBottomButton',
|
'#chatspace': '#chatBottomButton',
|
||||||
'#contactlist': '#contactListButton',
|
'#contactlist': '#contactListButton',
|
||||||
'#settingsmenu': '#toolbar_button_settings'
|
'#settingsmenu': '#toolbar_button_settings'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
var currentlyOpen = null;
|
||||||
|
|
||||||
|
/**
|
||||||
* Toggles the windows in the side panel
|
* Toggles the windows in the side panel
|
||||||
* @param object the window that should be shown
|
* @param object the window that should be shown
|
||||||
* @param selector the selector for the element containing the panel
|
* @param selector the selector for the element containing the panel
|
||||||
|
@ -28,14 +23,13 @@ var PanelToggler = (function(my) {
|
||||||
* @param onOpen function to be called if the window is going to be opened
|
* @param onOpen function to be called if the window is going to be opened
|
||||||
* @param onClose function to be called if the window is going to be closed
|
* @param onClose function to be called if the window is going to be closed
|
||||||
*/
|
*/
|
||||||
var toggle = function(object, selector, onOpenComplete, onOpen, onClose) {
|
function toggle (object, selector, onOpenComplete, onOpen, onClose) {
|
||||||
UIUtil.buttonClick(buttons[selector], "active");
|
UIUtil.buttonClick(buttons[selector], "active");
|
||||||
|
|
||||||
if (object.isVisible()) {
|
if (object.isVisible()) {
|
||||||
$("#toast-container").animate({
|
$("#toast-container").animate({
|
||||||
right: '5px'
|
right: 5
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
queue: false,
|
queue: false,
|
||||||
duration: 500
|
duration: 500
|
||||||
});
|
});
|
||||||
|
@ -49,15 +43,14 @@ var PanelToggler = (function(my) {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentlyOpen = null;
|
currentlyOpen = null;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Undock the toolbar when the chat is shown and if we're in a
|
// Undock the toolbar when the chat is shown and if we're in a
|
||||||
// video mode.
|
// video mode.
|
||||||
if (LargeVideo.isLargeVideoVisible()) {
|
if (VideoLayout.isLargeVideoVisible()) {
|
||||||
ToolbarToggler.dockToolbar(false);
|
ToolbarToggler.dockToolbar(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(currentlyOpen) {
|
if (currentlyOpen) {
|
||||||
var current = $(currentlyOpen);
|
var current = $(currentlyOpen);
|
||||||
UIUtil.buttonClick(buttons[currentlyOpen], "active");
|
UIUtil.buttonClick(buttons[currentlyOpen], "active");
|
||||||
current.css('z-index', 4);
|
current.css('z-index', 4);
|
||||||
|
@ -68,9 +61,8 @@ var PanelToggler = (function(my) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#toast-container").animate({
|
$("#toast-container").animate({
|
||||||
right: (PanelToggler.getPanelSize()[0] + 5) + 'px'
|
right: (UIUtil.getSidePanelSize()[0] + 5)
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
queue: false,
|
queue: false,
|
||||||
duration: 500
|
duration: 500
|
||||||
});
|
});
|
||||||
|
@ -86,14 +78,20 @@ var PanelToggler = (function(my) {
|
||||||
|
|
||||||
currentlyOpen = selector;
|
currentlyOpen = selector;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggler for the chat, contact list, settings menu, etc..
|
||||||
|
*/
|
||||||
|
var PanelToggler = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens / closes the chat area.
|
* Opens / closes the chat area.
|
||||||
*/
|
*/
|
||||||
my.toggleChat = function() {
|
toggleChat () {
|
||||||
var chatCompleteFunction = Chat.isVisible() ?
|
var chatCompleteFunction = Chat.isVisible()
|
||||||
function() {} : function () {
|
? function () {}
|
||||||
|
: function () {
|
||||||
Chat.scrollChatToBottom();
|
Chat.scrollChatToBottom();
|
||||||
$('#chatspace').trigger('shown');
|
$('#chatspace').trigger('shown');
|
||||||
};
|
};
|
||||||
|
@ -112,16 +110,24 @@ var PanelToggler = (function(my) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
Chat.resizeChat,
|
() => this.resizeChat(),
|
||||||
null);
|
null);
|
||||||
};
|
},
|
||||||
|
|
||||||
|
resizeChat () {
|
||||||
|
let [width, height] = UIUtil.getSidePanelSize();
|
||||||
|
Chat.resizeChat(width, height);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens / closes the contact list area.
|
* Opens / closes the contact list area.
|
||||||
*/
|
*/
|
||||||
my.toggleContactList = function () {
|
toggleContactList () {
|
||||||
var completeFunction = ContactList.isVisible() ?
|
var completeFunction = ContactList.isVisible()
|
||||||
function() {} : function () { $('#contactlist').trigger('shown');};
|
? function () {}
|
||||||
|
: function () {
|
||||||
|
$('#contactlist').trigger('shown');
|
||||||
|
};
|
||||||
VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
|
VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
|
||||||
|
|
||||||
toggle(ContactList,
|
toggle(ContactList,
|
||||||
|
@ -131,12 +137,12 @@ var PanelToggler = (function(my) {
|
||||||
ContactList.setVisualNotification(false);
|
ContactList.setVisualNotification(false);
|
||||||
},
|
},
|
||||||
null);
|
null);
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens / closes the settings menu
|
* Opens / closes the settings menu
|
||||||
*/
|
*/
|
||||||
my.toggleSettingsMenu = function() {
|
toggleSettingsMenu () {
|
||||||
VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){});
|
VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){});
|
||||||
toggle(SettingsMenu,
|
toggle(SettingsMenu,
|
||||||
'#settingsmenu',
|
'#settingsmenu',
|
||||||
|
@ -147,31 +153,13 @@ var PanelToggler = (function(my) {
|
||||||
$('#setEmail').get(0).value = settings.email;
|
$('#setEmail').get(0).value = settings.email;
|
||||||
},
|
},
|
||||||
null);
|
null);
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
isVisible () {
|
||||||
* Returns the size of the side panel.
|
|
||||||
*/
|
|
||||||
my.getPanelSize = function () {
|
|
||||||
var availableHeight = window.innerHeight;
|
|
||||||
var availableWidth = window.innerWidth;
|
|
||||||
|
|
||||||
var panelWidth = 200;
|
|
||||||
if (availableWidth * 0.2 < 200) {
|
|
||||||
panelWidth = availableWidth * 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [panelWidth, availableHeight];
|
|
||||||
};
|
|
||||||
|
|
||||||
my.isVisible = function() {
|
|
||||||
return (Chat.isVisible() ||
|
return (Chat.isVisible() ||
|
||||||
ContactList.isVisible() ||
|
ContactList.isVisible() ||
|
||||||
SettingsMenu.isVisible());
|
SettingsMenu.isVisible());
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return my;
|
export default PanelToggler;
|
||||||
|
|
||||||
}(PanelToggler || {}));
|
|
||||||
|
|
||||||
module.exports = PanelToggler;
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
/* global APP, $, Util, nickname:true */
|
/* global APP, $ */
|
||||||
var Replacement = require("./Replacement");
|
|
||||||
var CommandsProcessor = require("./Commands");
|
import {processReplacements, linkify} from './Replacement';
|
||||||
var ToolbarToggler = require("../../toolbars/ToolbarToggler");
|
import CommandsProcessor from './Commands';
|
||||||
|
import ToolbarToggler from '../../toolbars/ToolbarToggler';
|
||||||
|
|
||||||
|
import UIUtil from '../../util/UIUtil';
|
||||||
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
|
|
||||||
var smileys = require("./smileys.json").smileys;
|
var smileys = require("./smileys.json").smileys;
|
||||||
var NicknameHandler = require("../../util/NicknameHandler");
|
|
||||||
var UIUtil = require("../../util/UIUtil");
|
|
||||||
var UIEvents = require("../../../../service/UI/UIEvents");
|
|
||||||
|
|
||||||
var notificationInterval = false;
|
var notificationInterval = false;
|
||||||
var unreadMessages = 0;
|
var unreadMessages = 0;
|
||||||
|
@ -165,28 +167,21 @@ function resizeChatConversation() {
|
||||||
/**
|
/**
|
||||||
* Chat related user interface.
|
* Chat related user interface.
|
||||||
*/
|
*/
|
||||||
var Chat = (function (my) {
|
var Chat = {
|
||||||
/**
|
/**
|
||||||
* Initializes chat related interface.
|
* Initializes chat related interface.
|
||||||
*/
|
*/
|
||||||
my.init = function () {
|
init (eventEmitter) {
|
||||||
if(NicknameHandler.getNickname())
|
if (APP.settings.getDisplayName()) {
|
||||||
Chat.setChatConversationMode(true);
|
Chat.setChatConversationMode(true);
|
||||||
NicknameHandler.addListener(UIEvents.NICKNAME_CHANGED,
|
}
|
||||||
function (nickname) {
|
|
||||||
Chat.setChatConversationMode(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#nickinput').keydown(function (event) {
|
$('#nickinput').keydown(function (event) {
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var val = UIUtil.escapeHtml(this.value);
|
var val = UIUtil.escapeHtml(this.value);
|
||||||
this.value = '';
|
this.value = '';
|
||||||
if (!NicknameHandler.getNickname()) {
|
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val);
|
||||||
NicknameHandler.setNickname(val);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -197,14 +192,12 @@ var Chat = (function (my) {
|
||||||
var value = this.value;
|
var value = this.value;
|
||||||
usermsg.val('').trigger('autosize.resize');
|
usermsg.val('').trigger('autosize.resize');
|
||||||
this.focus();
|
this.focus();
|
||||||
var command = new CommandsProcessor(value);
|
var command = new CommandsProcessor(value, eventEmitter);
|
||||||
if(command.isCommand()) {
|
if (command.isCommand()) {
|
||||||
command.processCommand();
|
command.processCommand();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
var message = UIUtil.escapeHtml(value);
|
var message = UIUtil.escapeHtml(value);
|
||||||
APP.xmpp.sendChatMessage(message,
|
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
|
||||||
NicknameHandler.getNickname());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -222,19 +215,17 @@ var Chat = (function (my) {
|
||||||
});
|
});
|
||||||
|
|
||||||
addSmileys();
|
addSmileys();
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the given message to the chat conversation.
|
* Appends the given message to the chat conversation.
|
||||||
*/
|
*/
|
||||||
my.updateChatConversation =
|
updateChatConversation (id, displayName, message, stamp) {
|
||||||
function (from, displayName, message, myjid, stamp) {
|
|
||||||
var divClassName = '';
|
var divClassName = '';
|
||||||
|
|
||||||
if (APP.xmpp.myJid() === from) {
|
if (APP.conference.isLocalId(id)) {
|
||||||
divClassName = "localuser";
|
divClassName = "localuser";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
divClassName = "remoteuser";
|
divClassName = "remoteuser";
|
||||||
|
|
||||||
if (!Chat.isVisible()) {
|
if (!Chat.isVisible()) {
|
||||||
|
@ -250,7 +241,7 @@ var Chat = (function (my) {
|
||||||
var escMessage = message.replace(/</g, '<').
|
var escMessage = message.replace(/</g, '<').
|
||||||
replace(/>/g, '>').replace(/\n/g, '<br/>');
|
replace(/>/g, '>').replace(/\n/g, '<br/>');
|
||||||
var escDisplayName = UIUtil.escapeHtml(displayName);
|
var escDisplayName = UIUtil.escapeHtml(displayName);
|
||||||
message = Replacement.processReplacements(escMessage);
|
message = processReplacements(escMessage);
|
||||||
|
|
||||||
var messageContainer =
|
var messageContainer =
|
||||||
'<div class="chatmessage">'+
|
'<div class="chatmessage">'+
|
||||||
|
@ -263,14 +254,14 @@ var Chat = (function (my) {
|
||||||
$('#chatconversation').append(messageContainer);
|
$('#chatconversation').append(messageContainer);
|
||||||
$('#chatconversation').animate(
|
$('#chatconversation').animate(
|
||||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends error message to the conversation
|
* Appends error message to the conversation
|
||||||
* @param errorMessage the received error message.
|
* @param errorMessage the received error message.
|
||||||
* @param originalText the original message.
|
* @param originalText the original message.
|
||||||
*/
|
*/
|
||||||
my.chatAddError = function(errorMessage, originalText) {
|
chatAddError (errorMessage, originalText) {
|
||||||
errorMessage = UIUtil.escapeHtml(errorMessage);
|
errorMessage = UIUtil.escapeHtml(errorMessage);
|
||||||
originalText = UIUtil.escapeHtml(originalText);
|
originalText = UIUtil.escapeHtml(originalText);
|
||||||
|
|
||||||
|
@ -281,28 +272,28 @@ var Chat = (function (my) {
|
||||||
(errorMessage? (' Reason: ' + errorMessage) : '') + '</div>');
|
(errorMessage? (' Reason: ' + errorMessage) : '') + '</div>');
|
||||||
$('#chatconversation').animate(
|
$('#chatconversation').animate(
|
||||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the subject to the UI
|
* Sets the subject to the UI
|
||||||
* @param subject the subject
|
* @param subject the subject
|
||||||
*/
|
*/
|
||||||
my.chatSetSubject = function(subject) {
|
setSubject (subject) {
|
||||||
if (subject)
|
if (subject) {
|
||||||
subject = subject.trim();
|
subject = subject.trim();
|
||||||
$('#subject').html(Replacement.linkify(UIUtil.escapeHtml(subject)));
|
}
|
||||||
if(subject === "") {
|
$('#subject').html(linkify(UIUtil.escapeHtml(subject)));
|
||||||
|
if (subject) {
|
||||||
|
$("#subject").css({display: "block"});
|
||||||
|
} else {
|
||||||
$("#subject").css({display: "none"});
|
$("#subject").css({display: "none"});
|
||||||
}
|
}
|
||||||
else {
|
},
|
||||||
$("#subject").css({display: "block"});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the chat conversation mode.
|
* Sets the chat conversation mode.
|
||||||
*/
|
*/
|
||||||
my.setChatConversationMode = function (isConversationMode) {
|
setChatConversationMode (isConversationMode) {
|
||||||
if (isConversationMode) {
|
if (isConversationMode) {
|
||||||
$('#nickname').css({visibility: 'hidden'});
|
$('#nickname').css({visibility: 'hidden'});
|
||||||
$('#chatconversation').css({visibility: 'visible'});
|
$('#chatconversation').css({visibility: 'visible'});
|
||||||
|
@ -310,42 +301,37 @@ var Chat = (function (my) {
|
||||||
$('#smileysarea').css({visibility: 'visible'});
|
$('#smileysarea').css({visibility: 'visible'});
|
||||||
$('#usermsg').focus();
|
$('#usermsg').focus();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the chat area.
|
* Resizes the chat area.
|
||||||
*/
|
*/
|
||||||
my.resizeChat = function () {
|
resizeChat (width, height) {
|
||||||
var chatSize = require("../SidePanelToggler").getPanelSize();
|
$('#chatspace').width(width).height(height);
|
||||||
|
|
||||||
$('#chatspace').width(chatSize[0]);
|
|
||||||
$('#chatspace').height(chatSize[1]);
|
|
||||||
|
|
||||||
resizeChatConversation();
|
resizeChatConversation();
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the chat is currently visible.
|
* Indicates if the chat is currently visible.
|
||||||
*/
|
*/
|
||||||
my.isVisible = function () {
|
isVisible () {
|
||||||
return $('#chatspace').is(":visible");
|
return $('#chatspace').is(":visible");
|
||||||
};
|
},
|
||||||
/**
|
/**
|
||||||
* Shows and hides the window with the smileys
|
* Shows and hides the window with the smileys
|
||||||
*/
|
*/
|
||||||
my.toggleSmileys = toggleSmileys;
|
toggleSmileys,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scrolls chat to the bottom.
|
* Scrolls chat to the bottom.
|
||||||
*/
|
*/
|
||||||
my.scrollChatToBottom = function() {
|
scrollChatToBottom () {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$('#chatconversation').scrollTop(
|
$('#chatconversation').scrollTop(
|
||||||
$('#chatconversation')[0].scrollHeight);
|
$('#chatconversation')[0].scrollHeight);
|
||||||
}, 5);
|
}, 5);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Chat;
|
||||||
return my;
|
|
||||||
}(Chat || {}));
|
|
||||||
module.exports = Chat;
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
/* global APP, require */
|
/* global APP */
|
||||||
var UIUtil = require("../../util/UIUtil");
|
import UIUtil from '../../util/UIUtil';
|
||||||
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List with supported commands. The keys are the names of the commands and
|
* List with supported commands. The keys are the names of the commands and
|
||||||
* the value is the function that processes the message.
|
* the value is the function that processes the message.
|
||||||
* @type {{String: function}}
|
* @type {{String: function}}
|
||||||
*/
|
*/
|
||||||
var commands = {
|
const commands = {
|
||||||
"topic" : processTopic
|
"topic" : processTopic
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,9 +30,9 @@ function getCommand(message) {
|
||||||
* Processes the data for topic command.
|
* Processes the data for topic command.
|
||||||
* @param commandArguments the arguments of the topic command.
|
* @param commandArguments the arguments of the topic command.
|
||||||
*/
|
*/
|
||||||
function processTopic(commandArguments) {
|
function processTopic(commandArguments, emitter) {
|
||||||
var topic = UIUtil.escapeHtml(commandArguments);
|
var topic = UIUtil.escapeHtml(commandArguments);
|
||||||
APP.xmpp.setSubject(topic);
|
emitter.emit(UIEvents.SUBJECT_CHANGED, topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,9 +41,11 @@ function processTopic(commandArguments) {
|
||||||
* @param message the message
|
* @param message the message
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function CommandsProcessor(message) {
|
function CommandsProcessor(message, emitter) {
|
||||||
var command = getCommand(message);
|
var command = getCommand(message);
|
||||||
|
|
||||||
|
this.emitter = emitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the command.
|
* Returns the name of the command.
|
||||||
* @returns {String} the command
|
* @returns {String} the command
|
||||||
|
@ -80,7 +83,7 @@ CommandsProcessor.prototype.processCommand = function() {
|
||||||
if(!this.isCommand())
|
if(!this.isCommand())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
commands[this.getCommand()](this.getArgument());
|
commands[this.getCommand()](this.getArgument(), this.emitter);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = CommandsProcessor;
|
export default CommandsProcessor;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
var Smileys = require("./smileys.json");
|
var Smileys = require("./smileys.json");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes links and smileys in "body"
|
* Processes links and smileys in "body"
|
||||||
*/
|
*/
|
||||||
function processReplacements(body)
|
export function processReplacements(body) {
|
||||||
{
|
|
||||||
//make links clickable
|
//make links clickable
|
||||||
body = linkify(body);
|
body = linkify(body);
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@ function processReplacements(body)
|
||||||
* Finds and replaces all links in the links in "body"
|
* Finds and replaces all links in the links in "body"
|
||||||
* with their <a href=""></a>
|
* with their <a href=""></a>
|
||||||
*/
|
*/
|
||||||
function linkify(inputText)
|
export function linkify(inputText) {
|
||||||
{
|
|
||||||
var replacedText, replacePattern1, replacePattern2, replacePattern3;
|
var replacedText, replacePattern1, replacePattern2, replacePattern3;
|
||||||
|
|
||||||
//URLs starting with http://, https://, or ftp://
|
//URLs starting with http://, https://, or ftp://
|
||||||
|
@ -40,8 +39,7 @@ function linkify(inputText)
|
||||||
/**
|
/**
|
||||||
* Replaces common smiley strings with images
|
* Replaces common smiley strings with images
|
||||||
*/
|
*/
|
||||||
function smilify(body)
|
function smilify(body) {
|
||||||
{
|
|
||||||
if(!body) {
|
if(!body) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
@ -56,8 +54,3 @@ function smilify(body)
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
processReplacements: processReplacements,
|
|
||||||
linkify: linkify
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
/* global $, APP, Strophe */
|
/* global $, APP */
|
||||||
var Avatar = require('../../avatar/Avatar');
|
import Avatar from '../../avatar/Avatar';
|
||||||
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
|
|
||||||
var numberOfContacts = 0;
|
let numberOfContacts = 0;
|
||||||
var notificationInterval;
|
let notificationInterval;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the number of participants in the contact list button and sets
|
* Updates the number of participants in the contact list button and sets
|
||||||
|
@ -30,7 +31,7 @@ function updateNumberOfParticipants(delta) {
|
||||||
* @return {object} the newly created avatar element
|
* @return {object} the newly created avatar element
|
||||||
*/
|
*/
|
||||||
function createAvatar(jid) {
|
function createAvatar(jid) {
|
||||||
var avatar = document.createElement('img');
|
let avatar = document.createElement('img');
|
||||||
avatar.className = "icon-avatar avatar";
|
avatar.className = "icon-avatar avatar";
|
||||||
avatar.src = Avatar.getAvatarUrl(jid);
|
avatar.src = Avatar.getAvatarUrl(jid);
|
||||||
|
|
||||||
|
@ -43,12 +44,12 @@ function createAvatar(jid) {
|
||||||
* @param displayName the display name to set
|
* @param displayName the display name to set
|
||||||
*/
|
*/
|
||||||
function createDisplayNameParagraph(key, displayName) {
|
function createDisplayNameParagraph(key, displayName) {
|
||||||
var p = document.createElement('p');
|
let p = document.createElement('p');
|
||||||
if(displayName)
|
if (displayName) {
|
||||||
p.innerText = displayName;
|
p.innerHTML = displayName;
|
||||||
else if(key) {
|
} else if(key) {
|
||||||
p.setAttribute("data-i18n",key);
|
p.setAttribute("data-i18n",key);
|
||||||
p.innerText = APP.translation.translateString(key);
|
p.innerHTML = APP.translation.translateString(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
|
@ -64,93 +65,79 @@ function stopGlowing(glower) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getContactEl (id) {
|
||||||
|
return $(`#contacts>li[id="${id}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contactElExists (id) {
|
||||||
|
return getContactEl(id).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact list.
|
* Contact list.
|
||||||
*/
|
*/
|
||||||
var ContactList = {
|
var ContactList = {
|
||||||
|
init (emitter) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Indicates if the chat is currently visible.
|
* Indicates if the chat is currently visible.
|
||||||
*
|
*
|
||||||
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
|
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
|
||||||
* otherwise
|
* otherwise
|
||||||
*/
|
*/
|
||||||
isVisible: function () {
|
isVisible () {
|
||||||
return $('#contactlist').is(":visible");
|
return $('#contactlist').is(":visible");
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a contact for the given peerJid if such doesn't yet exist.
|
* Adds a contact for the given id.
|
||||||
*
|
*
|
||||||
* @param peerJid the peerJid corresponding to the contact
|
|
||||||
*/
|
*/
|
||||||
ensureAddContact: function (peerJid) {
|
addContact (id) {
|
||||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
let contactlist = $('#contacts');
|
||||||
|
|
||||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
let newContact = document.createElement('li');
|
||||||
|
newContact.id = id;
|
||||||
if (!contact || contact.length <= 0)
|
|
||||||
ContactList.addContact(peerJid);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a contact for the given peer jid.
|
|
||||||
*
|
|
||||||
* @param peerJid the jid of the contact to add
|
|
||||||
*/
|
|
||||||
addContact: function (peerJid) {
|
|
||||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
|
||||||
|
|
||||||
var contactlist = $('#contacts');
|
|
||||||
|
|
||||||
var newContact = document.createElement('li');
|
|
||||||
newContact.id = resourceJid;
|
|
||||||
newContact.className = "clickable";
|
newContact.className = "clickable";
|
||||||
newContact.onclick = function (event) {
|
newContact.onclick = (event) => {
|
||||||
if (event.currentTarget.className === "clickable") {
|
if (event.currentTarget.className === "clickable") {
|
||||||
$(ContactList).trigger('contactclicked', [peerJid]);
|
this.emitter.emit(UIEvents.CONTACT_CLICKED, id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
newContact.appendChild(createAvatar(peerJid));
|
newContact.appendChild(createAvatar(id));
|
||||||
newContact.appendChild(createDisplayNameParagraph("participant"));
|
newContact.appendChild(createDisplayNameParagraph("participant"));
|
||||||
|
|
||||||
if (resourceJid === APP.xmpp.myResource()) {
|
if (APP.conference.isLocalId(id)) {
|
||||||
contactlist.prepend(newContact);
|
contactlist.prepend(newContact);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
contactlist.append(newContact);
|
contactlist.append(newContact);
|
||||||
}
|
}
|
||||||
updateNumberOfParticipants(1);
|
updateNumberOfParticipants(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a contact for the given peer jid.
|
* Removes a contact for the given id.
|
||||||
*
|
*
|
||||||
* @param peerJid the peerJid corresponding to the contact to remove
|
|
||||||
*/
|
*/
|
||||||
removeContact: function (peerJid) {
|
removeContact (id) {
|
||||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
let contact = getContactEl(id);
|
||||||
|
|
||||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
|
||||||
|
|
||||||
if (contact && contact.length > 0) {
|
|
||||||
var contactlist = $('#contactlist>ul');
|
|
||||||
|
|
||||||
contactlist.get(0).removeChild(contact.get(0));
|
|
||||||
|
|
||||||
|
if (contact.length > 0) {
|
||||||
|
contact.remove();
|
||||||
updateNumberOfParticipants(-1);
|
updateNumberOfParticipants(-1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setVisualNotification: function (show, stopGlowingIn) {
|
setVisualNotification (show, stopGlowingIn) {
|
||||||
var glower = $('#contactListButton');
|
let glower = $('#contactListButton');
|
||||||
|
|
||||||
if (show && !notificationInterval) {
|
if (show && !notificationInterval) {
|
||||||
notificationInterval = window.setInterval(function () {
|
notificationInterval = window.setInterval(function () {
|
||||||
glower.toggleClass('active glowing');
|
glower.toggleClass('active glowing');
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
} else if (!show && notificationInterval) {
|
||||||
else if (!show && notificationInterval) {
|
|
||||||
stopGlowing(glower);
|
stopGlowing(glower);
|
||||||
}
|
}
|
||||||
if (stopGlowingIn) {
|
if (stopGlowingIn) {
|
||||||
|
@ -160,35 +147,28 @@ var ContactList = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setClickable: function (resourceJid, isClickable) {
|
setClickable (id, isClickable) {
|
||||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
getContactEl(id).toggleClass('clickable', isClickable);
|
||||||
if (isClickable) {
|
|
||||||
contact.addClass('clickable');
|
|
||||||
} else {
|
|
||||||
contact.removeClass('clickable');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onDisplayNameChange: function (peerJid, displayName) {
|
onDisplayNameChange (id, displayName) {
|
||||||
if (peerJid === 'localVideoContainer')
|
if (id === 'localVideoContainer') {
|
||||||
peerJid = APP.xmpp.myJid();
|
id = APP.conference.localId;
|
||||||
|
}
|
||||||
|
let contactName = $(`#contacts #${id}>p`);
|
||||||
|
|
||||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
if (displayName) {
|
||||||
|
|
||||||
var contactName = $('#contacts #' + resourceJid + '>p');
|
|
||||||
|
|
||||||
if (contactName && displayName && displayName.length > 0)
|
|
||||||
contactName.html(displayName);
|
contactName.html(displayName);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
userAvatarChanged: function (resourceJid, avatarUrl) {
|
changeUserAvatar (id, avatarUrl) {
|
||||||
// set the avatar in the contact list
|
// set the avatar in the contact list
|
||||||
var contact = $('#' + resourceJid + '>img');
|
let contact = $(`#${id}>img`);
|
||||||
if (contact && contact.length > 0) {
|
if (contact.length > 0) {
|
||||||
contact.get(0).src = avatarUrl;
|
contact.attr('src', avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ContactList;
|
export default ContactList;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/* global APP, $ */
|
/* global APP, $ */
|
||||||
var Avatar = require("../../avatar/Avatar");
|
import UIUtil from "../../util/UIUtil";
|
||||||
var Settings = require("./../../../settings/Settings");
|
import UIEvents from "../../../../service/UI/UIEvents";
|
||||||
var UIUtil = require("../../util/UIUtil");
|
import languages from "../../../../service/translation/languages";
|
||||||
var languages = require("../../../../service/translation/languages");
|
import Settings from '../../../settings/Settings';
|
||||||
|
|
||||||
function generateLanguagesSelectBox() {
|
function generateLanguagesSelectBox() {
|
||||||
var currentLang = APP.translation.getCurrentLanguage();
|
var currentLang = APP.translation.getCurrentLanguage();
|
||||||
var html = "<select id=\"languages_selectbox\">";
|
var html = '<select id="languages_selectbox">';
|
||||||
var langArray = languages.getLanguages();
|
var langArray = languages.getLanguages();
|
||||||
for(var i = 0; i < langArray.length; i++) {
|
for(var i = 0; i < langArray.length; i++) {
|
||||||
var lang = langArray[i];
|
var lang = langArray[i];
|
||||||
|
@ -22,32 +22,54 @@ function generateLanguagesSelectBox() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var SettingsMenu = {
|
export default {
|
||||||
|
init (emitter) {
|
||||||
|
function update() {
|
||||||
|
let displayName = UIUtil.escapeHtml($('#setDisplayName').val());
|
||||||
|
|
||||||
init: function () {
|
if (displayName && Settings.getDisplayName() !== displayName) {
|
||||||
var startMutedSelector = $("#startMutedOptions");
|
emitter.emit(UIEvents.NICKNAME_CHANGED, displayName);
|
||||||
startMutedSelector.before(generateLanguagesSelectBox());
|
}
|
||||||
|
|
||||||
|
let language = $("#languages_selectbox").val();
|
||||||
|
if (language !== Settings.getLanguage()) {
|
||||||
|
emitter.emit(UIEvents.LANG_CHANGED, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
let email = UIUtil.escapeHtml($('#setEmail').val());
|
||||||
|
if (email !== Settings.getEmail()) {
|
||||||
|
emitter.emit(UIEvents.EMAIL_CHANGED, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
let startAudioMuted = $("#startAudioMuted").is(":checked");
|
||||||
|
let startVideoMuted = $("#startVideoMuted").is(":checked");
|
||||||
|
if (startAudioMuted !== APP.conference.startAudioMuted
|
||||||
|
|| startVideoMuted !== APP.conference.startVideoMuted) {
|
||||||
|
emitter.emit(
|
||||||
|
UIEvents.START_MUTED_CHANGED,
|
||||||
|
startAudioMuted,
|
||||||
|
startVideoMuted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let startMutedBlock = $("#startMutedOptions");
|
||||||
|
startMutedBlock.before(generateLanguagesSelectBox());
|
||||||
APP.translation.translateElement($("#languages_selectbox"));
|
APP.translation.translateElement($("#languages_selectbox"));
|
||||||
|
|
||||||
|
this.onRoleChanged();
|
||||||
|
this.onStartMutedChanged();
|
||||||
|
|
||||||
|
$("#updateSettings").click(update);
|
||||||
$('#settingsmenu>input').keyup(function(event){
|
$('#settingsmenu>input').keyup(function(event){
|
||||||
if(event.keyCode === 13) {//enter
|
if (event.keyCode === 13) {//enter
|
||||||
SettingsMenu.update();
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (APP.xmpp.isModerator()) {
|
|
||||||
startMutedSelector.css("display", "block");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
startMutedSelector.css("display", "none");
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#updateSettings").click(function () {
|
|
||||||
SettingsMenu.update();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoleChanged: function () {
|
onRoleChanged () {
|
||||||
if(APP.xmpp.isModerator()) {
|
if(APP.conference.isModerator) {
|
||||||
$("#startMutedOptions").css("display", "block");
|
$("#startMutedOptions").css("display", "block");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -55,55 +77,22 @@ var SettingsMenu = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setStartMuted: function (audio, video) {
|
onStartMutedChanged () {
|
||||||
$("#startAudioMuted").attr("checked", audio);
|
$("#startAudioMuted").attr("checked", APP.conference.startAudioMuted);
|
||||||
$("#startVideoMuted").attr("checked", video);
|
$("#startVideoMuted").attr("checked", APP.conference.startVideoMuted);
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function() {
|
isVisible () {
|
||||||
var newDisplayName =
|
|
||||||
UIUtil.escapeHtml($('#setDisplayName').get(0).value);
|
|
||||||
var newEmail = UIUtil.escapeHtml($('#setEmail').get(0).value);
|
|
||||||
|
|
||||||
if(newDisplayName) {
|
|
||||||
var displayName = Settings.setDisplayName(newDisplayName);
|
|
||||||
APP.xmpp.addToPresence("displayName", displayName, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var language = $("#languages_selectbox").val();
|
|
||||||
APP.translation.setLanguage(language);
|
|
||||||
Settings.setLanguage(language);
|
|
||||||
|
|
||||||
APP.xmpp.addToPresence("email", newEmail);
|
|
||||||
var email = Settings.setEmail(newEmail);
|
|
||||||
|
|
||||||
var startAudioMuted = ($("#startAudioMuted").is(":checked"));
|
|
||||||
var startVideoMuted = ($("#startVideoMuted").is(":checked"));
|
|
||||||
APP.xmpp.addToPresence("startMuted",
|
|
||||||
[startAudioMuted, startVideoMuted]);
|
|
||||||
|
|
||||||
Avatar.setUserAvatar(APP.xmpp.myJid(), email);
|
|
||||||
},
|
|
||||||
|
|
||||||
isVisible: function() {
|
|
||||||
return $('#settingsmenu').is(':visible');
|
return $('#settingsmenu').is(':visible');
|
||||||
},
|
},
|
||||||
|
|
||||||
setDisplayName: function(newDisplayName) {
|
onDisplayNameChange (id, newDisplayName) {
|
||||||
var displayName = Settings.setDisplayName(newDisplayName);
|
if(id === 'localVideoContainer' || APP.conference.isLocalId(id)) {
|
||||||
$('#setDisplayName').get(0).value = displayName;
|
$('#setDisplayName').val(newDisplayName);
|
||||||
},
|
|
||||||
|
|
||||||
onDisplayNameChange: function(peerJid, newDisplayName) {
|
|
||||||
if(peerJid === 'localVideoContainer' ||
|
|
||||||
peerJid === APP.xmpp.myJid()) {
|
|
||||||
this.setDisplayName(newDisplayName);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeAvatar: function (thumbUrl) {
|
|
||||||
$('#avatar').get(0).src = thumbUrl;
|
changeAvatar (avatarUrl) {
|
||||||
|
$('#avatar').attr('src', avatarUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = SettingsMenu;
|
|
|
@ -1,66 +1,109 @@
|
||||||
/* global $ */
|
/* global $ */
|
||||||
var PanelToggler = require("../side_pannels/SidePanelToggler");
|
import UIUtil from '../util/UIUtil';
|
||||||
var UIUtil = require("../util/UIUtil");
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter");
|
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
|
||||||
var UIEvents = require("../../../service/UI/UIEvents");
|
|
||||||
|
|
||||||
var eventEmitter = null;
|
const defaultBottomToolbarButtons = {
|
||||||
|
|
||||||
var buttonHandlers = {
|
|
||||||
"bottom_toolbar_contact_list": function () {
|
|
||||||
AnalyticsAdapter.sendEvent('bottomtoolbar.contacts.toggled');
|
|
||||||
BottomToolbar.toggleContactList();
|
|
||||||
},
|
|
||||||
"bottom_toolbar_film_strip": function () {
|
|
||||||
AnalyticsAdapter.sendEvent('bottomtoolbar.filmstrip.toggled');
|
|
||||||
BottomToolbar.toggleFilmStrip();
|
|
||||||
},
|
|
||||||
"bottom_toolbar_chat": function () {
|
|
||||||
AnalyticsAdapter.sendEvent('bottomtoolbar.chat.toggled');
|
|
||||||
BottomToolbar.toggleChat();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var defaultBottomToolbarButtons = {
|
|
||||||
'chat': '#bottom_toolbar_chat',
|
'chat': '#bottom_toolbar_chat',
|
||||||
'contacts': '#bottom_toolbar_contact_list',
|
'contacts': '#bottom_toolbar_contact_list',
|
||||||
'filmstrip': '#bottom_toolbar_film_strip'
|
'filmstrip': '#bottom_toolbar_film_strip'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BottomToolbar = {
|
||||||
|
init () {
|
||||||
|
this.filmStrip = $('#remoteVideos');
|
||||||
|
this.toolbar = $('#bottomToolbar');
|
||||||
|
},
|
||||||
|
|
||||||
var BottomToolbar = (function (my) {
|
setupListeners (emitter) {
|
||||||
my.init = function (emitter) {
|
|
||||||
eventEmitter = emitter;
|
|
||||||
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
|
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
|
||||||
|
|
||||||
for(var k in buttonHandlers)
|
const buttonHandlers = {
|
||||||
$("#" + k).click(buttonHandlers[k]);
|
"bottom_toolbar_contact_list": function () {
|
||||||
|
AnalyticsAdapter.sendEvent('bottomtoolbar.contacts.toggled');
|
||||||
|
emitter.emit(UIEvents.TOGGLE_CONTACT_LIST);
|
||||||
|
},
|
||||||
|
"bottom_toolbar_film_strip": function () {
|
||||||
|
AnalyticsAdapter.sendEvent('bottomtoolbar.filmstrip.toggled');
|
||||||
|
emitter.emit(UIEvents.TOGGLE_FILM_STRIP);
|
||||||
|
},
|
||||||
|
"bottom_toolbar_chat": function () {
|
||||||
|
AnalyticsAdapter.sendEvent('bottomtoolbar.chat.toggled');
|
||||||
|
emitter.emit(UIEvents.TOGGLE_CHAT);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
my.toggleChat = function() {
|
Object.keys(buttonHandlers).forEach(
|
||||||
PanelToggler.toggleChat();
|
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
|
||||||
};
|
);
|
||||||
|
},
|
||||||
|
|
||||||
my.toggleContactList = function() {
|
toggleFilmStrip () {
|
||||||
PanelToggler.toggleContactList();
|
this.filmStrip.toggleClass("hidden");
|
||||||
};
|
},
|
||||||
|
|
||||||
my.toggleFilmStrip = function() {
|
isFilmStripVisible () {
|
||||||
var filmstrip = $("#remoteVideos");
|
return !this.filmStrip.hasClass('hidden');
|
||||||
filmstrip.toggleClass("hidden");
|
},
|
||||||
|
|
||||||
eventEmitter.emit( UIEvents.FILM_STRIP_TOGGLED,
|
setupFilmStripOnly () {
|
||||||
filmstrip.hasClass("hidden"));
|
this.filmStrip.css({
|
||||||
};
|
padding: "0px 0px 18px 0px",
|
||||||
|
right: 0
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
$(document).bind("remotevideo.resized", function (event, width, height) {
|
getFilmStripHeight () {
|
||||||
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
|
if (this.isFilmStripVisible()) {
|
||||||
|
return this.filmStrip.outerHeight();
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
$('#bottomToolbar').css({bottom: bottom + 'px'});
|
getFilmStripWidth () {
|
||||||
|
return this.filmStrip.width();
|
||||||
|
},
|
||||||
|
|
||||||
|
resizeThumbnails (thumbWidth, thumbHeight,
|
||||||
|
animate = false, forceUpdate = false) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.filmStrip.animate({
|
||||||
|
// adds 2 px because of small video 1px border
|
||||||
|
height: thumbHeight + 2
|
||||||
|
}, {
|
||||||
|
queue: false,
|
||||||
|
duration: animate ? 500 : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return my;
|
this.getThumbs(!forceUpdate).animate({
|
||||||
}(BottomToolbar || {}));
|
height: thumbHeight,
|
||||||
|
width: thumbWidth
|
||||||
|
}, {
|
||||||
|
queue: false,
|
||||||
|
duration: animate ? 500 : 0,
|
||||||
|
complete: resolve
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = BottomToolbar;
|
if (!animate) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resizeToolbar (thumbWidth, thumbHeight) {
|
||||||
|
let bottom = (thumbHeight - this.toolbar.outerHeight())/2 + 18;
|
||||||
|
this.toolbar.css({bottom});
|
||||||
|
},
|
||||||
|
|
||||||
|
getThumbs (only_visible = false) {
|
||||||
|
let selector = 'span';
|
||||||
|
if (only_visible) {
|
||||||
|
selector += ':visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.filmStrip.children(selector);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomToolbar;
|
||||||
|
|
|
@ -1,70 +1,134 @@
|
||||||
/* global APP, $, buttonClick, config, lockRoom, interfaceConfig, setSharedKey,
|
/* global APP, $, config, interfaceConfig */
|
||||||
Util */
|
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
var messageHandler = require("../util/MessageHandler");
|
import messageHandler from '../util/MessageHandler';
|
||||||
var BottomToolbar = require("./BottomToolbar");
|
import UIUtil from '../util/UIUtil';
|
||||||
var Prezi = require("../prezi/Prezi");
|
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
|
||||||
var Etherpad = require("../etherpad/Etherpad");
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
var PanelToggler = require("../side_pannels/SidePanelToggler");
|
|
||||||
var Authentication = require("../authentication/Authentication");
|
|
||||||
var UIUtil = require("../util/UIUtil");
|
|
||||||
var AuthenticationEvents
|
|
||||||
= require("../../../service/authentication/AuthenticationEvents");
|
|
||||||
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter");
|
|
||||||
var Feedback = require("../Feedback");
|
|
||||||
|
|
||||||
var roomUrl = null;
|
let roomUrl = null;
|
||||||
var sharedKey = '';
|
let recordingToaster = null;
|
||||||
var UI = null;
|
let emitter = null;
|
||||||
var recordingToaster = null;
|
|
||||||
|
|
||||||
var buttonHandlers = {
|
|
||||||
|
/**
|
||||||
|
* Opens the invite link dialog.
|
||||||
|
*/
|
||||||
|
function openLinkDialog () {
|
||||||
|
let inviteAttributes;
|
||||||
|
|
||||||
|
if (roomUrl === null) {
|
||||||
|
inviteAttributes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
|
||||||
|
APP.translation.translateString("roomUrlDefaultMsg") + '"';
|
||||||
|
} else {
|
||||||
|
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
|
||||||
|
}
|
||||||
|
messageHandler.openTwoButtonDialog(
|
||||||
|
"dialog.shareLink", null, null,
|
||||||
|
`<input id="inviteLinkRef" type="text" ${inviteAttributes} onclick="this.select();" readonly>`,
|
||||||
|
false, "dialog.Invite",
|
||||||
|
function (e, v) {
|
||||||
|
if (v && roomUrl) {
|
||||||
|
emitter.emit(UIEvents.USER_INVITED, roomUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function (event) {
|
||||||
|
if (roomUrl) {
|
||||||
|
document.getElementById('inviteLinkRef').select();
|
||||||
|
} else {
|
||||||
|
if (event && event.target) {
|
||||||
|
$(event.target).find('button[value=true]').prop('disabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the state of the recording button
|
||||||
|
function setRecordingButtonState (recordingState) {
|
||||||
|
let selector = $('#toolbar_button_record');
|
||||||
|
|
||||||
|
if (recordingState === 'on') {
|
||||||
|
selector.removeClass("icon-recEnable");
|
||||||
|
selector.addClass("icon-recEnable active");
|
||||||
|
|
||||||
|
$("#largeVideo").toggleClass("videoMessageFilter", true);
|
||||||
|
let recordOnKey = "recording.on";
|
||||||
|
$('#videoConnectionMessage').attr("data-i18n", recordOnKey);
|
||||||
|
$('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
$("#largeVideo").toggleClass("videoMessageFilter", false);
|
||||||
|
$('#videoConnectionMessage').css({display: "none"});
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
recordingToaster = messageHandler.notify(
|
||||||
|
null, "recording.toaster", null,
|
||||||
|
null, null,
|
||||||
|
{timeOut: 0, closeButton: null, tapToDismiss: false}
|
||||||
|
);
|
||||||
|
} else if (recordingState === 'off') {
|
||||||
|
selector.removeClass("icon-recEnable active");
|
||||||
|
selector.addClass("icon-recEnable");
|
||||||
|
|
||||||
|
$("#largeVideo").toggleClass("videoMessageFilter", false);
|
||||||
|
$('#videoConnectionMessage').css({display: "none"});
|
||||||
|
|
||||||
|
if (recordingToaster) {
|
||||||
|
messageHandler.remove(recordingToaster);
|
||||||
|
}
|
||||||
|
} else if (recordingState === 'pending') {
|
||||||
|
selector.removeClass("icon-recEnable active");
|
||||||
|
selector.addClass("icon-recEnable");
|
||||||
|
|
||||||
|
$("#largeVideo").toggleClass("videoMessageFilter", true);
|
||||||
|
let recordPendingKey = "recording.pending";
|
||||||
|
$('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
|
||||||
|
$('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
|
||||||
|
$('#videoConnectionMessage').css({display: "block"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonHandlers = {
|
||||||
"toolbar_button_mute": function () {
|
"toolbar_button_mute": function () {
|
||||||
if (APP.RTC.localAudio.isMuted()) {
|
if (APP.conference.audioMuted) {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.audio.unmuted');
|
AnalyticsAdapter.sendEvent('toolbar.audio.unmuted');
|
||||||
|
emitter.emit(UIEvents.AUDIO_MUTED, false);
|
||||||
} else {
|
} else {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.audio.muted');
|
AnalyticsAdapter.sendEvent('toolbar.audio.muted');
|
||||||
|
emitter.emit(UIEvents.AUDIO_MUTED, true);
|
||||||
}
|
}
|
||||||
return APP.UI.toggleAudio();
|
|
||||||
},
|
},
|
||||||
"toolbar_button_camera": function () {
|
"toolbar_button_camera": function () {
|
||||||
if (APP.RTC.localVideo.isMuted()) {
|
if (APP.conference.videoMuted) {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.video.enabled');
|
AnalyticsAdapter.sendEvent('toolbar.video.enabled');
|
||||||
|
emitter.emit(UIEvents.VIDEO_MUTED, false);
|
||||||
} else {
|
} else {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.video.disabled');
|
AnalyticsAdapter.sendEvent('toolbar.video.disabled');
|
||||||
|
emitter.emit(UIEvents.VIDEO_MUTED, true);
|
||||||
}
|
}
|
||||||
return APP.UI.toggleVideo();
|
|
||||||
},
|
},
|
||||||
/*"toolbar_button_authentication": function () {
|
|
||||||
return Toolbar.authenticateClicked();
|
|
||||||
},*/
|
|
||||||
"toolbar_button_record": function () {
|
"toolbar_button_record": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.recording.toggled');
|
AnalyticsAdapter.sendEvent('toolbar.recording.toggled');
|
||||||
return toggleRecording();
|
emitter.emit(UIEvents.RECORDING_TOGGLE);
|
||||||
},
|
},
|
||||||
"toolbar_button_security": function () {
|
"toolbar_button_security": function () {
|
||||||
if (sharedKey) {
|
emitter.emit(UIEvents.ROOM_LOCK_CLICKED);
|
||||||
AnalyticsAdapter.sendEvent('toolbar.lock.disabled');
|
|
||||||
} else {
|
|
||||||
AnalyticsAdapter.sendEvent('toolbar.lock.enabled');
|
|
||||||
}
|
|
||||||
return Toolbar.openLockDialog();
|
|
||||||
},
|
},
|
||||||
"toolbar_button_link": function () {
|
"toolbar_button_link": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.invite.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.invite.clicked');
|
||||||
return Toolbar.openLinkDialog();
|
openLinkDialog();
|
||||||
},
|
},
|
||||||
"toolbar_button_chat": function () {
|
"toolbar_button_chat": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.chat.toggled');
|
AnalyticsAdapter.sendEvent('toolbar.chat.toggled');
|
||||||
return BottomToolbar.toggleChat();
|
emitter.emit(UIEvents.TOGGLE_CHAT);
|
||||||
},
|
},
|
||||||
"toolbar_button_prezi": function () {
|
"toolbar_button_prezi": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.prezi.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.prezi.clicked');
|
||||||
return Prezi.openPreziDialog();
|
emitter.emit(UIEvents.PREZI_CLICKED);
|
||||||
},
|
},
|
||||||
"toolbar_button_etherpad": function () {
|
"toolbar_button_etherpad": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
|
||||||
return Etherpad.toggleEtherpad(0);
|
emitter.emit(UIEvents.ETHERPAD_CLICKED);
|
||||||
},
|
},
|
||||||
"toolbar_button_desktopsharing": function () {
|
"toolbar_button_desktopsharing": function () {
|
||||||
if (APP.desktopsharing.isUsingScreenStream) {
|
if (APP.desktopsharing.isUsingScreenStream) {
|
||||||
|
@ -72,32 +136,32 @@ var buttonHandlers = {
|
||||||
} else {
|
} else {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.screen.enabled');
|
AnalyticsAdapter.sendEvent('toolbar.screen.enabled');
|
||||||
}
|
}
|
||||||
return APP.desktopsharing.toggleScreenSharing();
|
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||||
},
|
},
|
||||||
"toolbar_button_fullScreen": function() {
|
"toolbar_button_fullScreen": function() {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled');
|
AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled');
|
||||||
UIUtil.buttonClick("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
|
UIUtil.buttonClick("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
|
||||||
return Toolbar.toggleFullScreen();
|
emitter.emit(UIEvents.FULLSCREEN_TOGGLE);
|
||||||
},
|
},
|
||||||
"toolbar_button_sip": function () {
|
"toolbar_button_sip": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.sip.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.sip.clicked');
|
||||||
return callSipButtonClicked();
|
showSipNumberInput();
|
||||||
},
|
},
|
||||||
"toolbar_button_dialpad": function () {
|
"toolbar_button_dialpad": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.sip.dialpad.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.sip.dialpad.clicked');
|
||||||
return dialpadButtonClicked();
|
dialpadButtonClicked();
|
||||||
},
|
},
|
||||||
"toolbar_button_settings": function () {
|
"toolbar_button_settings": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.settings.toggled');
|
AnalyticsAdapter.sendEvent('toolbar.settings.toggled');
|
||||||
PanelToggler.toggleSettingsMenu();
|
emitter.emit(UIEvents.TOGGLE_SETTINGS);
|
||||||
},
|
},
|
||||||
"toolbar_button_hangup": function () {
|
"toolbar_button_hangup": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.hangup');
|
AnalyticsAdapter.sendEvent('toolbar.hangup');
|
||||||
return hangup();
|
emitter.emit(UIEvents.HANGUP);
|
||||||
},
|
},
|
||||||
"toolbar_button_login": function () {
|
"toolbar_button_login": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.authenticate.login.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.authenticate.login.clicked');
|
||||||
Toolbar.authenticateClicked();
|
emitter.emit(UIEvents.AUTH_CLICKED);
|
||||||
},
|
},
|
||||||
"toolbar_button_logout": function () {
|
"toolbar_button_logout": function () {
|
||||||
AnalyticsAdapter.sendEvent('toolbar.authenticate.logout.clicked');
|
AnalyticsAdapter.sendEvent('toolbar.authenticate.logout.clicked');
|
||||||
|
@ -111,18 +175,13 @@ var buttonHandlers = {
|
||||||
"dialog.Yes",
|
"dialog.Yes",
|
||||||
function (evt, yes) {
|
function (evt, yes) {
|
||||||
if (yes) {
|
if (yes) {
|
||||||
APP.xmpp.logout(function (url) {
|
emitter.emit(UIEvents.LOGOUT);
|
||||||
if (url) {
|
|
||||||
window.location.href = url;
|
|
||||||
} else {
|
|
||||||
hangup();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var defaultToolbarButtons = {
|
const defaultToolbarButtons = {
|
||||||
'microphone': '#toolbar_button_mute',
|
'microphone': '#toolbar_button_mute',
|
||||||
'camera': '#toolbar_button_camera',
|
'camera': '#toolbar_button_camera',
|
||||||
'desktop': '#toolbar_button_desktopsharing',
|
'desktop': '#toolbar_button_desktopsharing',
|
||||||
|
@ -136,606 +195,198 @@ var defaultToolbarButtons = {
|
||||||
'hangup': '#toolbar_button_hangup'
|
'hangup': '#toolbar_button_hangup'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Hangs up this call.
|
|
||||||
*/
|
|
||||||
function hangup() {
|
|
||||||
var conferenceDispose = function () {
|
|
||||||
APP.xmpp.disposeConference();
|
|
||||||
|
|
||||||
if (config.enableWelcomePage) {
|
|
||||||
setTimeout(function() {
|
|
||||||
window.localStorage.welcomePageDisabled = false;
|
|
||||||
window.location.pathname = "/";
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Feedback.isEnabled())
|
|
||||||
{
|
|
||||||
// If the user has already entered feedback, we'll show the window and
|
|
||||||
// immidiately start the conference dispose timeout.
|
|
||||||
if (Feedback.feedbackScore > 0) {
|
|
||||||
Feedback.openFeedbackWindow();
|
|
||||||
conferenceDispose();
|
|
||||||
|
|
||||||
}
|
|
||||||
// Otherwise we'll wait for user's feedback.
|
|
||||||
else
|
|
||||||
Feedback.openFeedbackWindow(conferenceDispose);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
conferenceDispose();
|
|
||||||
|
|
||||||
// If the feedback functionality isn't enabled we show a thank you
|
|
||||||
// dialog.
|
|
||||||
APP.UI.messageHandler.openMessageDialog(null, null, null,
|
|
||||||
APP.translation.translateString("dialog.thankYou",
|
|
||||||
{appName:interfaceConfig.APP_NAME}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts or stops the recording for the conference.
|
|
||||||
*/
|
|
||||||
function toggleRecording(predefinedToken) {
|
|
||||||
APP.xmpp.toggleRecording(function (callback) {
|
|
||||||
if (predefinedToken) {
|
|
||||||
callback(UIUtil.escapeHtml(predefinedToken));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.recordingToken");
|
|
||||||
var token = APP.translation.translateString("dialog.token");
|
|
||||||
APP.UI.messageHandler.openTwoButtonDialog(null, null, null,
|
|
||||||
'<h2>' + msg + '</h2>' +
|
|
||||||
'<input name="recordingToken" type="text" ' +
|
|
||||||
' data-i18n="[placeholder]dialog.token" ' +
|
|
||||||
'placeholder="' + token + '" autofocus>',
|
|
||||||
false,
|
|
||||||
"dialog.Save",
|
|
||||||
function (e, v, m, f) {
|
|
||||||
if (v) {
|
|
||||||
var token = f.recordingToken;
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
callback(UIUtil.escapeHtml(token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
function () { },
|
|
||||||
':input:first'
|
|
||||||
);
|
|
||||||
}, Toolbar.setRecordingButtonState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Locks / unlocks the room.
|
|
||||||
*/
|
|
||||||
function lockRoom(lock) {
|
|
||||||
var currentSharedKey = '';
|
|
||||||
if (lock)
|
|
||||||
currentSharedKey = sharedKey;
|
|
||||||
|
|
||||||
APP.xmpp.lockRoom(currentSharedKey, function (res) {
|
|
||||||
// password is required
|
|
||||||
if (sharedKey) {
|
|
||||||
console.log('set room password');
|
|
||||||
Toolbar.lockLockButton();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log('removed room password');
|
|
||||||
Toolbar.unlockLockButton();
|
|
||||||
}
|
|
||||||
}, function (err) {
|
|
||||||
console.warn('setting password failed', err);
|
|
||||||
messageHandler.showError("dialog.lockTitle",
|
|
||||||
"dialog.lockMessage");
|
|
||||||
Toolbar.setSharedKey('');
|
|
||||||
}, function () {
|
|
||||||
console.warn('room passwords not supported');
|
|
||||||
messageHandler.showError("dialog.warning",
|
|
||||||
"dialog.passwordNotSupported");
|
|
||||||
Toolbar.setSharedKey('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invite participants to conference.
|
|
||||||
*/
|
|
||||||
function inviteParticipants() {
|
|
||||||
if (roomUrl === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var sharedKeyText = "";
|
|
||||||
if (sharedKey && sharedKey.length > 0) {
|
|
||||||
sharedKeyText =
|
|
||||||
APP.translation.translateString("email.sharedKey",
|
|
||||||
{sharedKey: sharedKey});
|
|
||||||
sharedKeyText = sharedKeyText.replace(/\n/g, "%0D%0A");
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportedBrowsers = "Chromium, Google Chrome " +
|
|
||||||
APP.translation.translateString("email.and") + " Opera";
|
|
||||||
var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
|
|
||||||
var subject = APP.translation.translateString("email.subject",
|
|
||||||
{appName:interfaceConfig.APP_NAME, conferenceName: conferenceName});
|
|
||||||
var body = APP.translation.translateString("email.body",
|
|
||||||
{appName:interfaceConfig.APP_NAME, sharedKeyText: sharedKeyText,
|
|
||||||
roomUrl: roomUrl, supportedBrowsers: supportedBrowsers});
|
|
||||||
body = body.replace(/\n/g, "%0D%0A");
|
|
||||||
|
|
||||||
if (window.localStorage.displayname) {
|
|
||||||
body += "%0D%0A%0D%0A" + window.localStorage.displayname;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interfaceConfig.INVITATION_POWERED_BY) {
|
|
||||||
body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
function dialpadButtonClicked() {
|
function dialpadButtonClicked() {
|
||||||
//TODO show the dialpad box
|
//TODO show the dialpad box
|
||||||
}
|
}
|
||||||
|
|
||||||
function callSipButtonClicked() {
|
function showSipNumberInput () {
|
||||||
var defaultNumber
|
let defaultNumber = config.defaultSipNumber
|
||||||
= config.defaultSipNumber ? config.defaultSipNumber : '';
|
? config.defaultSipNumber
|
||||||
|
: '';
|
||||||
|
|
||||||
var sipMsg = APP.translation.generateTranslationHTML(
|
let sipMsg = APP.translation.generateTranslationHTML("dialog.sipMsg");
|
||||||
"dialog.sipMsg");
|
messageHandler.openTwoButtonDialog(
|
||||||
messageHandler.openTwoButtonDialog(null, null, null,
|
null, null, null,
|
||||||
'<h2>' + sipMsg + '</h2>' +
|
`<h2>${sipMsg}</h2>
|
||||||
'<input name="sipNumber" type="text"' +
|
<input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
|
||||||
' value="' + defaultNumber + '" autofocus>',
|
false, "dialog.Dial",
|
||||||
false,
|
|
||||||
"dialog.Dial",
|
|
||||||
function (e, v, m, f) {
|
function (e, v, m, f) {
|
||||||
if (v) {
|
if (v && f.sipNumber) {
|
||||||
var numberInput = f.sipNumber;
|
emitter.emit(UIEvents.SIP_DIAL, f.sipNumber);
|
||||||
if (numberInput) {
|
|
||||||
APP.xmpp.dial(
|
|
||||||
numberInput, 'fromnumber', UI.getRoomName(), sharedKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null, null, ':input:first'
|
null, null, ':input:first'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var Toolbar = (function (my) {
|
const Toolbar = {
|
||||||
|
init (eventEmitter) {
|
||||||
|
emitter = eventEmitter;
|
||||||
|
|
||||||
my.init = function (ui) {
|
|
||||||
UIUtil.hideDisabledButtons(defaultToolbarButtons);
|
UIUtil.hideDisabledButtons(defaultToolbarButtons);
|
||||||
|
|
||||||
for(var k in buttonHandlers)
|
Object.keys(buttonHandlers).forEach(
|
||||||
$("#" + k).click(buttonHandlers[k]);
|
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
|
||||||
UI = ui;
|
|
||||||
// Update login info
|
|
||||||
APP.xmpp.addListener(
|
|
||||||
AuthenticationEvents.IDENTITY_UPDATED,
|
|
||||||
function (authenticationEnabled, userIdentity) {
|
|
||||||
|
|
||||||
var loggedIn = false;
|
|
||||||
if (userIdentity) {
|
|
||||||
loggedIn = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toolbar.showAuthenticateButton(authenticationEnabled);
|
|
||||||
|
|
||||||
if (authenticationEnabled) {
|
|
||||||
Toolbar.setAuthenticatedIdentity(userIdentity);
|
|
||||||
|
|
||||||
Toolbar.showLoginButton(!loggedIn);
|
|
||||||
Toolbar.showLogoutButton(loggedIn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets shared key
|
|
||||||
* @param sKey the shared key
|
|
||||||
*/
|
|
||||||
my.setSharedKey = function (sKey) {
|
|
||||||
sharedKey = sKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
my.authenticateClicked = function () {
|
|
||||||
Authentication.focusAuthenticationWindow();
|
|
||||||
if (!APP.xmpp.isExternalAuthEnabled()) {
|
|
||||||
Authentication.xmppAuthenticate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get authentication URL
|
|
||||||
if (!APP.xmpp.isMUCJoined()) {
|
|
||||||
APP.xmpp.getLoginUrl(UI.getRoomName(), function (url) {
|
|
||||||
// If conference has not been started yet - redirect to login page
|
|
||||||
window.location.href = url;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
APP.xmpp.getPopupLoginUrl(UI.getRoomName(), function (url) {
|
|
||||||
// Otherwise - open popup with authentication URL
|
|
||||||
var authenticationWindow = Authentication.createAuthenticationWindow(
|
|
||||||
function () {
|
|
||||||
// On popup closed - retry room allocation
|
|
||||||
APP.xmpp.allocateConferenceFocus(
|
|
||||||
APP.UI.getRoomName(),
|
|
||||||
function () { console.info("AUTH DONE"); }
|
|
||||||
);
|
|
||||||
}, url);
|
|
||||||
if (!authenticationWindow) {
|
|
||||||
messageHandler.openMessageDialog(
|
|
||||||
null, "dialog.popupError");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the room invite url.
|
* Updates the room invite url.
|
||||||
*/
|
*/
|
||||||
my.updateRoomUrl = function (newRoomUrl) {
|
updateRoomUrl (newRoomUrl) {
|
||||||
roomUrl = newRoomUrl;
|
roomUrl = newRoomUrl;
|
||||||
|
|
||||||
// If the invite dialog has been already opened we update the information.
|
// If the invite dialog has been already opened we update the information.
|
||||||
var inviteLink = document.getElementById('inviteLinkRef');
|
let inviteLink = document.getElementById('inviteLinkRef');
|
||||||
if (inviteLink) {
|
if (inviteLink) {
|
||||||
inviteLink.value = roomUrl;
|
inviteLink.value = roomUrl;
|
||||||
inviteLink.select();
|
inviteLink.select();
|
||||||
$('#inviteLinkRef').parent()
|
$('#inviteLinkRef').parent()
|
||||||
.find('button[value=true]').prop('disabled', false);
|
.find('button[value=true]').prop('disabled', false);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables and enables some of the buttons.
|
* Disables and enables some of the buttons.
|
||||||
*/
|
*/
|
||||||
my.setupButtonsFromConfig = function () {
|
setupButtonsFromConfig () {
|
||||||
if (!UIUtil.isButtonEnabled('prezi')) {
|
if (!UIUtil.isButtonEnabled('prezi')) {
|
||||||
$("#toolbar_button_prezi").css({display: "none"});
|
$("#toolbar_button_prezi").css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the lock room dialog.
|
|
||||||
*/
|
|
||||||
my.openLockDialog = function () {
|
|
||||||
// Only the focus is able to set a shared key.
|
|
||||||
if (!APP.xmpp.isModerator()) {
|
|
||||||
if (sharedKey) {
|
|
||||||
messageHandler.openMessageDialog(null,
|
|
||||||
"dialog.passwordError");
|
|
||||||
} else {
|
|
||||||
messageHandler.openMessageDialog(null, "dialog.passwordError2");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (sharedKey) {
|
|
||||||
messageHandler.openTwoButtonDialog(null, null,
|
|
||||||
"dialog.passwordCheck",
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
"dialog.Remove",
|
|
||||||
function (e, v) {
|
|
||||||
if (v) {
|
|
||||||
Toolbar.setSharedKey('');
|
|
||||||
lockRoom(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var msg = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.passwordMsg");
|
|
||||||
var yourPassword = APP.translation.translateString(
|
|
||||||
"dialog.yourPassword");
|
|
||||||
messageHandler.openTwoButtonDialog(null, null, null,
|
|
||||||
'<h2>' + msg + '</h2>' +
|
|
||||||
'<input name="lockKey" type="text"' +
|
|
||||||
' data-i18n="[placeholder]dialog.yourPassword" ' +
|
|
||||||
'placeholder="' + yourPassword + '" autofocus>',
|
|
||||||
false,
|
|
||||||
"dialog.Save",
|
|
||||||
function (e, v, m, f) {
|
|
||||||
if (v) {
|
|
||||||
var lockKey = f.lockKey;
|
|
||||||
|
|
||||||
if (lockKey) {
|
|
||||||
Toolbar.setSharedKey(
|
|
||||||
UIUtil.escapeHtml(lockKey));
|
|
||||||
lockRoom(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
null, null, 'input:first'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the invite link dialog.
|
|
||||||
*/
|
|
||||||
my.openLinkDialog = function () {
|
|
||||||
var inviteAttributes;
|
|
||||||
|
|
||||||
if (roomUrl === null) {
|
|
||||||
inviteAttributes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
|
|
||||||
APP.translation.translateString("roomUrlDefaultMsg") + '"';
|
|
||||||
} else {
|
|
||||||
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
|
|
||||||
}
|
|
||||||
messageHandler.openTwoButtonDialog("dialog.shareLink",
|
|
||||||
null, null,
|
|
||||||
'<input id="inviteLinkRef" type="text" ' +
|
|
||||||
inviteAttributes + ' onclick="this.select();" readonly>',
|
|
||||||
false,
|
|
||||||
"dialog.Invite",
|
|
||||||
function (e, v) {
|
|
||||||
if (v) {
|
|
||||||
if (roomUrl) {
|
|
||||||
inviteParticipants();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (event) {
|
|
||||||
if (roomUrl) {
|
|
||||||
document.getElementById('inviteLinkRef').select();
|
|
||||||
} else {
|
|
||||||
if (event && event.target)
|
|
||||||
$(event.target)
|
|
||||||
.find('button[value=true]').prop('disabled', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the settings dialog.
|
|
||||||
* FIXME: not used ?
|
|
||||||
*/
|
|
||||||
my.openSettingsDialog = function () {
|
|
||||||
var settings1 = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.settings1");
|
|
||||||
var settings2 = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.settings2");
|
|
||||||
var settings3 = APP.translation.generateTranslationHTML(
|
|
||||||
"dialog.settings3");
|
|
||||||
|
|
||||||
var yourPassword = APP.translation.translateString(
|
|
||||||
"dialog.yourPassword");
|
|
||||||
|
|
||||||
messageHandler.openTwoButtonDialog(null,
|
|
||||||
'<h2>' + settings1 + '</h2>' +
|
|
||||||
'<input type="checkbox" id="initMuted">' +
|
|
||||||
settings2 + '<br/>' +
|
|
||||||
'<input type="checkbox" id="requireNicknames">' +
|
|
||||||
settings3 +
|
|
||||||
'<input id="lockKey" type="text" placeholder="' + yourPassword +
|
|
||||||
'" data-i18n="[placeholder]dialog.yourPassword" autofocus>',
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
"dialog.Save",
|
|
||||||
function () {
|
|
||||||
document.getElementById('lockKey').focus();
|
|
||||||
},
|
|
||||||
function (e, v) {
|
|
||||||
if (v) {
|
|
||||||
if ($('#initMuted').is(":checked")) {
|
|
||||||
// it is checked
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($('#requireNicknames').is(":checked")) {
|
|
||||||
// it is checked
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var lockKey = document.getElementById('lockKey');
|
|
||||||
|
|
||||||
if (lockKey.value) {
|
|
||||||
setSharedKey(lockKey.value);
|
|
||||||
lockRoom(true);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the application in and out of full screen mode
|
|
||||||
* (a.k.a. presentation mode in Chrome).
|
|
||||||
*/
|
|
||||||
my.toggleFullScreen = function () {
|
|
||||||
var fsElement = document.documentElement;
|
|
||||||
|
|
||||||
if (!document.mozFullScreen && !document.webkitIsFullScreen) {
|
|
||||||
//Enter Full Screen
|
|
||||||
if (fsElement.mozRequestFullScreen) {
|
|
||||||
fsElement.mozRequestFullScreen();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//Exit Full Screen
|
|
||||||
if (document.mozCancelFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
} else {
|
|
||||||
document.webkitCancelFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Unlocks the lock button state.
|
* Unlocks the lock button state.
|
||||||
*/
|
*/
|
||||||
my.unlockLockButton = function () {
|
unlockLockButton () {
|
||||||
if ($("#toolbar_button_security").hasClass("icon-security-locked"))
|
if ($("#toolbar_button_security").hasClass("icon-security-locked"))
|
||||||
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the lock button state to locked.
|
* Updates the lock button state to locked.
|
||||||
*/
|
*/
|
||||||
my.lockLockButton = function () {
|
lockLockButton () {
|
||||||
if ($("#toolbar_button_security").hasClass("icon-security"))
|
if ($("#toolbar_button_security").hasClass("icon-security"))
|
||||||
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows or hides authentication button
|
* Shows or hides authentication button
|
||||||
* @param show <tt>true</tt> to show or <tt>false</tt> to hide
|
* @param show <tt>true</tt> to show or <tt>false</tt> to hide
|
||||||
*/
|
*/
|
||||||
my.showAuthenticateButton = function (show) {
|
showAuthenticateButton (show) {
|
||||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||||
$('#authentication').css({display: "inline"});
|
$('#authentication').css({display: "inline"});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$('#authentication').css({display: "none"});
|
$('#authentication').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
|
showEtherpadButton () {
|
||||||
|
if (!$('#toolbar_button_etherpad').is(":visible")) {
|
||||||
|
$('#toolbar_button_etherpad').css({display: 'inline-block'});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Shows or hides the 'recording' button.
|
// Shows or hides the 'recording' button.
|
||||||
my.showRecordingButton = function (show) {
|
showRecordingButton (show) {
|
||||||
if (UIUtil.isButtonEnabled('recording') && show) {
|
if (UIUtil.isButtonEnabled('recording') && show) {
|
||||||
$('#toolbar_button_record').css({display: "inline-block"});
|
$('#toolbar_button_record').css({display: "inline-block"});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$('#toolbar_button_record').css({display: "none"});
|
$('#toolbar_button_record').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
// Sets the state of the recording button
|
|
||||||
my.setRecordingButtonState = function (recordingState) {
|
|
||||||
var selector = $('#toolbar_button_record');
|
|
||||||
|
|
||||||
if (recordingState === 'on') {
|
|
||||||
selector.removeClass("icon-recEnable");
|
|
||||||
selector.addClass("icon-recEnable active");
|
|
||||||
|
|
||||||
$("#largeVideo").toggleClass("videoMessageFilter", true);
|
|
||||||
var recordOnKey = "recording.on";
|
|
||||||
$('#videoConnectionMessage').attr("data-i18n", recordOnKey);
|
|
||||||
$('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
|
|
||||||
|
|
||||||
setTimeout(function(){
|
|
||||||
$("#largeVideo").toggleClass("videoMessageFilter", false);
|
|
||||||
$('#videoConnectionMessage').css({display: "none"});
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
recordingToaster = messageHandler.notify(null, "recording.toaster", null,
|
|
||||||
null, null, {timeOut: 0, closeButton: null, tapToDismiss: false});
|
|
||||||
} else if (recordingState === 'off') {
|
|
||||||
selector.removeClass("icon-recEnable active");
|
|
||||||
selector.addClass("icon-recEnable");
|
|
||||||
|
|
||||||
$("#largeVideo").toggleClass("videoMessageFilter", false);
|
|
||||||
$('#videoConnectionMessage').css({display: "none"});
|
|
||||||
|
|
||||||
if (recordingToaster)
|
|
||||||
messageHandler.remove(recordingToaster);
|
|
||||||
|
|
||||||
} else if (recordingState === 'pending') {
|
|
||||||
selector.removeClass("icon-recEnable active");
|
|
||||||
selector.addClass("icon-recEnable");
|
|
||||||
|
|
||||||
$("#largeVideo").toggleClass("videoMessageFilter", true);
|
|
||||||
var recordPendingKey = "recording.pending";
|
|
||||||
$('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
|
|
||||||
$('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
|
|
||||||
$('#videoConnectionMessage').css({display: "block"});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// checks whether recording is enabled and whether we have params
|
// checks whether recording is enabled and whether we have params
|
||||||
// to start automatically recording
|
// to start automatically recording
|
||||||
my.checkAutoRecord = function () {
|
checkAutoRecord () {
|
||||||
if (UIUtil.isButtonEnabled('recording') && config.autoRecord) {
|
if (UIUtil.isButtonEnabled('recording') && config.autoRecord) {
|
||||||
toggleRecording(config.autoRecordToken);
|
emitter.emit(UIEvents.RECORDING_TOGGLE, UIUtil.escapeHtml(config.autoRecordToken));
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
// checks whether desktop sharing is enabled and whether
|
// checks whether desktop sharing is enabled and whether
|
||||||
// we have params to start automatically sharing
|
// we have params to start automatically sharing
|
||||||
my.checkAutoEnableDesktopSharing = function () {
|
checkAutoEnableDesktopSharing () {
|
||||||
if (UIUtil.isButtonEnabled('desktop')
|
if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) {
|
||||||
&& config.autoEnableDesktopSharing) {
|
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||||
APP.desktopsharing.toggleScreenSharing();
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
// Shows or hides SIP calls button
|
// Shows or hides SIP calls button
|
||||||
my.showSipCallButton = function (show) {
|
showSipCallButton (show) {
|
||||||
if (APP.xmpp.isSipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
|
if (APP.conference.sipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
|
||||||
$('#toolbar_button_sip').css({display: "inline-block"});
|
$('#toolbar_button_sip').css({display: "inline-block"});
|
||||||
} else {
|
} else {
|
||||||
$('#toolbar_button_sip').css({display: "none"});
|
$('#toolbar_button_sip').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
// Shows or hides the dialpad button
|
// Shows or hides the dialpad button
|
||||||
my.showDialPadButton = function (show) {
|
showDialPadButton (show) {
|
||||||
if (UIUtil.isButtonEnabled('dialpad') && show) {
|
if (UIUtil.isButtonEnabled('dialpad') && show) {
|
||||||
$('#toolbar_button_dialpad').css({display: "inline-block"});
|
$('#toolbar_button_dialpad').css({display: "inline-block"});
|
||||||
} else {
|
} else {
|
||||||
$('#toolbar_button_dialpad').css({display: "none"});
|
$('#toolbar_button_dialpad').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays user authenticated identity name(login).
|
* Displays user authenticated identity name(login).
|
||||||
* @param authIdentity identity name to be displayed.
|
* @param authIdentity identity name to be displayed.
|
||||||
*/
|
*/
|
||||||
my.setAuthenticatedIdentity = function (authIdentity) {
|
setAuthenticatedIdentity (authIdentity) {
|
||||||
if (authIdentity) {
|
if (authIdentity) {
|
||||||
var selector = $('#toolbar_auth_identity');
|
let selector = $('#toolbar_auth_identity');
|
||||||
selector.css({display: "list-item"});
|
selector.css({display: "list-item"});
|
||||||
selector.text(authIdentity);
|
selector.text(authIdentity);
|
||||||
} else {
|
} else {
|
||||||
$('#toolbar_auth_identity').css({display: "none"});
|
$('#toolbar_auth_identity').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows/hides login button.
|
* Shows/hides login button.
|
||||||
* @param show <tt>true</tt> to show
|
* @param show <tt>true</tt> to show
|
||||||
*/
|
*/
|
||||||
my.showLoginButton = function (show) {
|
showLoginButton (show) {
|
||||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||||
$('#toolbar_button_login').css({display: "list-item"});
|
$('#toolbar_button_login').css({display: "list-item"});
|
||||||
} else {
|
} else {
|
||||||
$('#toolbar_button_login').css({display: "none"});
|
$('#toolbar_button_login').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows/hides logout button.
|
* Shows/hides logout button.
|
||||||
* @param show <tt>true</tt> to show
|
* @param show <tt>true</tt> to show
|
||||||
*/
|
*/
|
||||||
my.showLogoutButton = function (show) {
|
showLogoutButton (show) {
|
||||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||||
$('#toolbar_button_logout').css({display: "list-item"});
|
$('#toolbar_button_logout').css({display: "list-item"});
|
||||||
} else {
|
} else {
|
||||||
$('#toolbar_button_logout').css({display: "none"});
|
$('#toolbar_button_logout').css({display: "none"});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the state of the button. The button has blue glow if desktop
|
* Sets the state of the button. The button has blue glow if desktop
|
||||||
* streaming is active.
|
* streaming is active.
|
||||||
* @param active the state of the desktop streaming.
|
* @param active the state of the desktop streaming.
|
||||||
*/
|
*/
|
||||||
my.changeDesktopSharingButtonState = function (active) {
|
changeDesktopSharingButtonState (active) {
|
||||||
var button = $("#toolbar_button_desktopsharing");
|
let button = $("#toolbar_button_desktopsharing");
|
||||||
if (active) {
|
if (active) {
|
||||||
button.addClass("glow");
|
button.addClass("glow");
|
||||||
} else {
|
} else {
|
||||||
button.removeClass("glow");
|
button.removeClass("glow");
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
return my;
|
updateRecordingState (state) {
|
||||||
}(Toolbar || {}));
|
setRecordingButtonState(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = Toolbar;
|
export default Toolbar;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
/* global APP, config, $, interfaceConfig, Moderator,
|
/* global APP, config, $, interfaceConfig */
|
||||||
DesktopStreaming.showDesktopSharingButton */
|
|
||||||
|
|
||||||
var toolbarTimeoutObject,
|
import UIUtil from '../util/UIUtil';
|
||||||
toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT,
|
import BottomToolbar from './BottomToolbar';
|
||||||
UIUtil = require("../util/UIUtil");
|
|
||||||
|
let toolbarTimeoutObject;
|
||||||
|
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
|
||||||
|
|
||||||
function showDesktopSharingButton() {
|
function showDesktopSharingButton() {
|
||||||
if (APP.desktopsharing.isDesktopSharingEnabled() &&
|
if (APP.desktopsharing.isDesktopSharingEnabled() &&
|
||||||
|
@ -14,19 +15,24 @@ function showDesktopSharingButton() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isToolbarVisible () {
|
||||||
|
return $('#header').is(':visible');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the toolbar.
|
* Hides the toolbar.
|
||||||
*/
|
*/
|
||||||
function hideToolbar() {
|
function hideToolbar() {
|
||||||
if(config.alwaysVisibleToolbar)
|
if (config.alwaysVisibleToolbar) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var header = $("#header"),
|
let header = $("#header");
|
||||||
bottomToolbar = $("#bottomToolbar");
|
let bottomToolbar = $("#bottomToolbar");
|
||||||
var isToolbarHover = false;
|
let isToolbarHover = false;
|
||||||
header.find('*').each(function () {
|
header.find('*').each(function () {
|
||||||
var id = $(this).attr('id');
|
let id = $(this).attr('id');
|
||||||
if ($("#" + id + ":hover").length > 0) {
|
if ($(`#${id}:hover`).length > 0) {
|
||||||
isToolbarHover = true;
|
isToolbarHover = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -37,34 +43,36 @@ function hideToolbar() {
|
||||||
clearTimeout(toolbarTimeoutObject);
|
clearTimeout(toolbarTimeoutObject);
|
||||||
toolbarTimeoutObject = null;
|
toolbarTimeoutObject = null;
|
||||||
|
|
||||||
if (!isToolbarHover) {
|
if (isToolbarHover) {
|
||||||
|
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
||||||
|
} else {
|
||||||
header.hide("slide", { direction: "up", duration: 300});
|
header.hide("slide", { direction: "up", duration: 300});
|
||||||
$('#subject').animate({top: "-=40"}, 300);
|
$('#subject').animate({top: "-=40"}, 300);
|
||||||
if ($("#remoteVideos").hasClass("hidden")) {
|
if (!BottomToolbar.isFilmStripVisible()) {
|
||||||
bottomToolbar.hide(
|
bottomToolbar.hide(
|
||||||
"slide", {direction: "right", duration: 300});
|
"slide", {direction: "right", duration: 300}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ToolbarToggler = {
|
const ToolbarToggler = {
|
||||||
/**
|
/**
|
||||||
* Shows the main toolbar.
|
* Shows the main toolbar.
|
||||||
*/
|
*/
|
||||||
showToolbar: function () {
|
showToolbar () {
|
||||||
if (interfaceConfig.filmStripOnly)
|
if (interfaceConfig.filmStripOnly) {
|
||||||
return;
|
return;
|
||||||
var header = $("#header"),
|
}
|
||||||
bottomToolbar = $("#bottomToolbar");
|
let header = $("#header");
|
||||||
|
let bottomToolbar = $("#bottomToolbar");
|
||||||
if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
|
if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
|
||||||
header.show("slide", { direction: "up", duration: 300});
|
header.show("slide", { direction: "up", duration: 300});
|
||||||
$('#subject').animate({top: "+=40"}, 300);
|
$('#subject').animate({top: "+=40"}, 300);
|
||||||
if (!bottomToolbar.is(":visible")) {
|
if (!bottomToolbar.is(":visible")) {
|
||||||
bottomToolbar.show(
|
bottomToolbar.show(
|
||||||
"slide", {direction: "right", duration: 300});
|
"slide", {direction: "right", duration: 300}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolbarTimeoutObject) {
|
if (toolbarTimeoutObject) {
|
||||||
|
@ -75,13 +83,6 @@ var ToolbarToggler = {
|
||||||
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
|
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (APP.xmpp.isModerator())
|
|
||||||
{
|
|
||||||
// TODO: Enable settings functionality.
|
|
||||||
// Need to uncomment the settings button in index.html.
|
|
||||||
// $('#settingsButton').css({visibility:"visible"});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show/hide desktop sharing button
|
// Show/hide desktop sharing button
|
||||||
showDesktopSharingButton();
|
showDesktopSharingButton();
|
||||||
},
|
},
|
||||||
|
@ -91,33 +92,28 @@ var ToolbarToggler = {
|
||||||
*
|
*
|
||||||
* @param isDock indicates what operation to perform
|
* @param isDock indicates what operation to perform
|
||||||
*/
|
*/
|
||||||
dockToolbar: function (isDock) {
|
dockToolbar (isDock) {
|
||||||
if (interfaceConfig.filmStripOnly)
|
if (interfaceConfig.filmStripOnly) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDock) {
|
if (isDock) {
|
||||||
// First make sure the toolbar is shown.
|
// First make sure the toolbar is shown.
|
||||||
if (!$('#header').is(':visible')) {
|
if (!isToolbarVisible()) {
|
||||||
this.showToolbar();
|
this.showToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then clear the time out, to dock the toolbar.
|
// Then clear the time out, to dock the toolbar.
|
||||||
if (toolbarTimeoutObject) {
|
|
||||||
clearTimeout(toolbarTimeoutObject);
|
clearTimeout(toolbarTimeoutObject);
|
||||||
toolbarTimeoutObject = null;
|
toolbarTimeoutObject = null;
|
||||||
}
|
} else {
|
||||||
}
|
if (isToolbarVisible()) {
|
||||||
else {
|
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
||||||
if (!$('#header').is(':visible')) {
|
} else {
|
||||||
this.showToolbar();
|
this.showToolbar();
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
showDesktopSharingButton: showDesktopSharingButton
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ToolbarToggler;
|
module.exports = ToolbarToggler;
|
|
@ -1,30 +0,0 @@
|
||||||
var UIEvents = require("../../../service/UI/UIEvents");
|
|
||||||
|
|
||||||
var nickname = null;
|
|
||||||
var eventEmitter = null;
|
|
||||||
|
|
||||||
var NicknameHandler = {
|
|
||||||
init: function (emitter) {
|
|
||||||
eventEmitter = emitter;
|
|
||||||
var storedDisplayName = window.localStorage.displayname;
|
|
||||||
if (storedDisplayName) {
|
|
||||||
nickname = storedDisplayName;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setNickname: function (newNickname) {
|
|
||||||
if (!newNickname || nickname === newNickname)
|
|
||||||
return;
|
|
||||||
|
|
||||||
nickname = newNickname;
|
|
||||||
window.localStorage.displayname = nickname;
|
|
||||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newNickname);
|
|
||||||
},
|
|
||||||
getNickname: function () {
|
|
||||||
return nickname;
|
|
||||||
},
|
|
||||||
addListener: function (type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = NicknameHandler;
|
|
|
@ -1,17 +1,34 @@
|
||||||
/* global $, config, interfaceConfig */
|
/* global $, config, interfaceConfig */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by hristo on 12/22/14.
|
* Created by hristo on 12/22/14.
|
||||||
*/
|
*/
|
||||||
var UIUtil = module.exports = {
|
var UIUtil = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the side panel.
|
||||||
|
*/
|
||||||
|
getSidePanelSize () {
|
||||||
|
var availableHeight = window.innerHeight;
|
||||||
|
var availableWidth = window.innerWidth;
|
||||||
|
|
||||||
|
var panelWidth = 200;
|
||||||
|
if (availableWidth * 0.2 < 200) {
|
||||||
|
panelWidth = availableWidth * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [panelWidth, availableHeight];
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the available video width.
|
* Returns the available video width.
|
||||||
*/
|
*/
|
||||||
getAvailableVideoWidth: function (isVisible) {
|
getAvailableVideoWidth: function (isSidePanelVisible) {
|
||||||
var PanelToggler = require("../side_pannels/SidePanelToggler");
|
let rightPanelWidth = 0;
|
||||||
if(typeof isVisible === "undefined" || isVisible === null)
|
|
||||||
isVisible = PanelToggler.isVisible();
|
if (isSidePanelVisible) {
|
||||||
var rightPanelWidth
|
rightPanelWidth = UIUtil.getSidePanelSize()[0];
|
||||||
= isVisible ? PanelToggler.getPanelSize()[0] : 0;
|
}
|
||||||
|
|
||||||
return window.innerWidth - rightPanelWidth;
|
return window.innerWidth - rightPanelWidth;
|
||||||
},
|
},
|
||||||
|
@ -112,5 +129,17 @@ var UIUtil = module.exports = {
|
||||||
.filter(function (item) { return item; })
|
.filter(function (item) { return item; })
|
||||||
.join(',');
|
.join(',');
|
||||||
$(selector).hide();
|
$(selector).hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
redirect (url) {
|
||||||
|
window.location.href = url;
|
||||||
|
},
|
||||||
|
|
||||||
|
isFullScreen () {
|
||||||
|
return document.fullScreen
|
||||||
|
|| document.mozFullScreen
|
||||||
|
|| document.webkitIsFullScreen;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default UIUtil;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
/* global APP, $ */
|
/* global APP, $ */
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
var JitsiPopover = require("../util/JitsiPopover");
|
import JitsiPopover from "../util/JitsiPopover";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs new connection indicator.
|
* Constructs new connection indicator.
|
||||||
* @param videoContainer the video container associated with the indicator.
|
* @param videoContainer the video container associated with the indicator.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function ConnectionIndicator(videoContainer, jid) {
|
function ConnectionIndicator(videoContainer, id) {
|
||||||
this.videoContainer = videoContainer;
|
this.videoContainer = videoContainer;
|
||||||
this.bandwidth = null;
|
this.bandwidth = null;
|
||||||
this.packetLoss = null;
|
this.packetLoss = null;
|
||||||
|
@ -16,7 +16,7 @@ function ConnectionIndicator(videoContainer, jid) {
|
||||||
this.resolution = null;
|
this.resolution = null;
|
||||||
this.transport = [];
|
this.transport = [];
|
||||||
this.popover = null;
|
this.popover = null;
|
||||||
this.jid = jid;
|
this.id = id;
|
||||||
this.create();
|
this.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolutionValue = null;
|
var resolutionValue = null;
|
||||||
if(this.resolution && this.jid) {
|
if(this.resolution && this.id) {
|
||||||
var keys = Object.keys(this.resolution);
|
var keys = Object.keys(this.resolution);
|
||||||
for(var ssrc in this.resolution) {
|
for(var ssrc in this.resolution) {
|
||||||
// skip resolutions for ssrc that don't have this info
|
// skip resolutions for ssrc that don't have this info
|
||||||
|
@ -99,7 +99,7 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.jid === null) {
|
if(this.id === null) {
|
||||||
resolution = "";
|
resolution = "";
|
||||||
if(this.resolution === null || !Object.keys(this.resolution) ||
|
if(this.resolution === null || !Object.keys(this.resolution) ||
|
||||||
Object.keys(this.resolution).length === 0) {
|
Object.keys(this.resolution).length === 0) {
|
||||||
|
@ -144,8 +144,8 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||||
if(this.videoContainer.videoSpanId == "localVideoContainer") {
|
if(this.videoContainer.videoSpanId == "localVideoContainer") {
|
||||||
result += "<div class=\"jitsipopover_showmore\" " +
|
result += "<div class=\"jitsipopover_showmore\" " +
|
||||||
"onclick = \"APP.UI.connectionIndicatorShowMore('" +
|
"onclick = \"APP.UI.connectionIndicatorShowMore('" +
|
||||||
// FIXME: we do not know local jid when this text is generated
|
// FIXME: we do not know local id when this text is generated
|
||||||
//this.jid + "')\" data-i18n='connectionindicator." +
|
//this.id + "')\" data-i18n='connectionindicator." +
|
||||||
"local')\" data-i18n='connectionindicator." +
|
"local')\" data-i18n='connectionindicator." +
|
||||||
(this.showMoreValue ? "less" : "more") + "'>" +
|
(this.showMoreValue ? "less" : "more") + "'>" +
|
||||||
translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
|
translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
|
||||||
|
@ -365,7 +365,8 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
||||||
*/
|
*/
|
||||||
ConnectionIndicator.prototype.updatePopoverData = function () {
|
ConnectionIndicator.prototype.updatePopoverData = function () {
|
||||||
this.popover.updateContent(
|
this.popover.updateContent(
|
||||||
"<div class=\"connection_info\">" + this.generateText() + "</div>");
|
`<div class="connection_info">${this.generateText()}</div>`
|
||||||
|
);
|
||||||
APP.translation.translateElement($(".connection_info"));
|
APP.translation.translateElement($(".connection_info"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -385,4 +386,4 @@ ConnectionIndicator.prototype.hideIndicator = function () {
|
||||||
this.popover.forceHide();
|
this.popover.forceHide();
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ConnectionIndicator;
|
export default ConnectionIndicator;
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all Large containers which we can show.
|
||||||
|
*/
|
||||||
|
export default class LargeContainer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show this container.
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
show () {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide this container.
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
hide () {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize this container.
|
||||||
|
* @param {number} containerWidth available width
|
||||||
|
* @param {number} containerHeight available height
|
||||||
|
* @param {boolean} animate if container should animate it's resize process
|
||||||
|
*/
|
||||||
|
resize (containerWidth, containerHeight, animate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for "hover in" events.
|
||||||
|
*/
|
||||||
|
onHoverIn (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for "hover out" events.
|
||||||
|
*/
|
||||||
|
onHoverOut (e) {
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +1,23 @@
|
||||||
/* global $, interfaceConfig, APP */
|
/* global $, interfaceConfig, APP */
|
||||||
var SmallVideo = require("./SmallVideo");
|
import ConnectionIndicator from "./ConnectionIndicator";
|
||||||
var ConnectionIndicator = require("./ConnectionIndicator");
|
import UIUtil from "../util/UIUtil";
|
||||||
var NicknameHandler = require("../util/NicknameHandler");
|
import UIEvents from "../../../service/UI/UIEvents";
|
||||||
var UIUtil = require("../util/UIUtil");
|
import SmallVideo from "./SmallVideo";
|
||||||
|
|
||||||
var LargeVideo = require("./LargeVideo");
|
var LargeVideo = require("./LargeVideo");
|
||||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||||
|
|
||||||
function LocalVideo(VideoLayout) {
|
const TrackEvents = JitsiMeetJS.events.track;
|
||||||
|
|
||||||
|
function LocalVideo(VideoLayout, emitter) {
|
||||||
this.videoSpanId = "localVideoContainer";
|
this.videoSpanId = "localVideoContainer";
|
||||||
this.container = $("#localVideoContainer").get(0);
|
this.container = $("#localVideoContainer").get(0);
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
this.VideoLayout = VideoLayout;
|
this.VideoLayout = VideoLayout;
|
||||||
this.flipX = true;
|
this.flipX = true;
|
||||||
this.isLocal = true;
|
this.isLocal = true;
|
||||||
this.peerJid = null;
|
this.emitter = emitter;
|
||||||
|
SmallVideo.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
||||||
|
@ -61,6 +65,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
|
||||||
$('#localDisplayName').html(defaultLocalDisplayName);
|
$('#localDisplayName').html(defaultLocalDisplayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.updateView();
|
||||||
} else {
|
} else {
|
||||||
var editButton = createEditDisplayNameButton();
|
var editButton = createEditDisplayNameButton();
|
||||||
|
|
||||||
|
@ -117,12 +122,14 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
|
||||||
|
|
||||||
editDisplayName.one("focusout", function (e) {
|
editDisplayName.one("focusout", function (e) {
|
||||||
self.VideoLayout.inputDisplayNameHandler(this.value);
|
self.VideoLayout.inputDisplayNameHandler(this.value);
|
||||||
|
$('#editDisplayName').hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
editDisplayName.on('keydown', function (e) {
|
editDisplayName.on('keydown', function (e) {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
self.VideoLayout.inputDisplayNameHandler(this.value);
|
$('#editDisplayName').hide();
|
||||||
|
// focusout handler will save display name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -130,25 +137,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalVideo.prototype.inputDisplayNameHandler = function (name) {
|
LocalVideo.prototype.inputDisplayNameHandler = function (name) {
|
||||||
name = UIUtil.escapeHtml(name);
|
this.emitter.emit(UIEvents.NICKNAME_CHANGED, UIUtil.escapeHtml(name));
|
||||||
|
|
||||||
NicknameHandler.setNickname(name);
|
|
||||||
|
|
||||||
var localDisplayName = $('#localDisplayName');
|
|
||||||
if (!localDisplayName.is(":visible")) {
|
|
||||||
if (NicknameHandler.getNickname()) {
|
|
||||||
var meHTML = APP.translation.generateTranslationHTML("me");
|
|
||||||
localDisplayName.html(NicknameHandler.getNickname() + " (" +
|
|
||||||
meHTML + ")");
|
|
||||||
} else {
|
|
||||||
var defaultHTML = APP.translation.generateTranslationHTML(
|
|
||||||
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
|
|
||||||
localDisplayName .html(defaultHTML);
|
|
||||||
}
|
|
||||||
localDisplayName.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#editDisplayName').hide();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalVideo.prototype.createConnectionIndicator = function() {
|
LocalVideo.prototype.createConnectionIndicator = function() {
|
||||||
|
@ -158,37 +147,29 @@ LocalVideo.prototype.createConnectionIndicator = function() {
|
||||||
this.connectionIndicator = new ConnectionIndicator(this, null);
|
this.connectionIndicator = new ConnectionIndicator(this, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
LocalVideo.prototype.changeVideo = function (stream) {
|
||||||
var self = this;
|
this.stream = stream;
|
||||||
|
|
||||||
function localVideoClick(event) {
|
let localVideoClick = (event) => {
|
||||||
// FIXME: with Temasys plugin event arg is not an event, but
|
// FIXME: with Temasys plugin event arg is not an event, but
|
||||||
// the clicked object itself, so we have to skip this call
|
// the clicked object itself, so we have to skip this call
|
||||||
if (event.stopPropagation) {
|
if (event.stopPropagation) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
self.VideoLayout.handleVideoThumbClicked(
|
this.VideoLayout.handleVideoThumbClicked(true, this.id);
|
||||||
true,
|
};
|
||||||
APP.xmpp.myResource());
|
|
||||||
}
|
|
||||||
|
|
||||||
var localVideoContainerSelector = $('#localVideoContainer');
|
let localVideoContainerSelector = $('#localVideoContainer');
|
||||||
localVideoContainerSelector.off('click');
|
localVideoContainerSelector.off('click');
|
||||||
localVideoContainerSelector.on('click', localVideoClick);
|
localVideoContainerSelector.on('click', localVideoClick);
|
||||||
|
|
||||||
if(isMuted) {
|
this.flipX = stream.videoType != "desktop";
|
||||||
APP.UI.setVideoMute(true);
|
let localVideo = document.createElement('video');
|
||||||
return;
|
localVideo.id = 'localVideo_' + stream.getId();
|
||||||
}
|
|
||||||
this.flipX = stream.videoType != "screen";
|
|
||||||
var localVideo = document.createElement('video');
|
|
||||||
localVideo.id = 'localVideo_' +
|
|
||||||
APP.RTC.getStreamID(stream.getOriginalStream());
|
|
||||||
if (!RTCBrowserType.isIExplorer()) {
|
if (!RTCBrowserType.isIExplorer()) {
|
||||||
localVideo.autoplay = true;
|
localVideo.autoplay = true;
|
||||||
localVideo.volume = 0; // is it required if audio is separated ?
|
localVideo.volume = 0; // is it required if audio is separated ?
|
||||||
}
|
}
|
||||||
localVideo.oncontextmenu = function () { return false; };
|
|
||||||
|
|
||||||
var localVideoContainer = document.getElementById('localVideoWrapper');
|
var localVideoContainer = document.getElementById('localVideoWrapper');
|
||||||
// Put the new video always in front
|
// Put the new video always in front
|
||||||
|
@ -207,29 +188,19 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach WebRTC stream
|
// Attach WebRTC stream
|
||||||
APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream());
|
stream.attach(localVideoSelector);
|
||||||
|
|
||||||
// Add stream ended handler
|
let endedHandler = () => {
|
||||||
APP.RTC.addMediaStreamInactiveHandler(
|
|
||||||
stream.getOriginalStream(), function () {
|
|
||||||
// We have to re-select after attach when Temasys plugin is used,
|
|
||||||
// because <video> element is replaced with <object>
|
|
||||||
localVideo = $('#' + localVideo.id)[0];
|
localVideo = $('#' + localVideo.id)[0];
|
||||||
localVideoContainer.removeChild(localVideo);
|
localVideoContainer.removeChild(localVideo);
|
||||||
self.VideoLayout.updateRemovedVideo(APP.xmpp.myResource());
|
this.VideoLayout.updateRemovedVideo(this.id);
|
||||||
});
|
stream.off(TrackEvents.TRACK_STOPPED, endedHandler);
|
||||||
|
};
|
||||||
|
stream.on(TrackEvents.TRACK_STOPPED, endedHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalVideo.prototype.joined = function (jid) {
|
LocalVideo.prototype.joined = function (id) {
|
||||||
this.peerJid = jid;
|
this.id = id;
|
||||||
};
|
};
|
||||||
|
|
||||||
LocalVideo.prototype.getResourceJid = function () {
|
export default LocalVideo;
|
||||||
var myResource = APP.xmpp.myResource();
|
|
||||||
if (!myResource) {
|
|
||||||
console.error("Requested local resource before we're in the MUC");
|
|
||||||
}
|
|
||||||
return myResource;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LocalVideo;
|
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
/* global $, APP, require, Strophe, interfaceConfig */
|
/* global $, APP, interfaceConfig */
|
||||||
var ConnectionIndicator = require("./ConnectionIndicator");
|
|
||||||
var SmallVideo = require("./SmallVideo");
|
|
||||||
var AudioLevels = require("../audio_levels/AudioLevels");
|
|
||||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
|
||||||
var UIUtils = require("../util/UIUtil");
|
|
||||||
var XMPPEvents = require("../../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
function RemoteVideo(peerJid, VideoLayout) {
|
import ConnectionIndicator from './ConnectionIndicator';
|
||||||
this.peerJid = peerJid;
|
|
||||||
this.resourceJid = Strophe.getResourceFromJid(peerJid);
|
import SmallVideo from "./SmallVideo";
|
||||||
this.videoSpanId = 'participant_' + this.resourceJid;
|
import AudioLevels from "../audio_levels/AudioLevels";
|
||||||
|
import UIUtils from "../util/UIUtil";
|
||||||
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
|
|
||||||
|
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||||
|
|
||||||
|
function RemoteVideo(id, VideoLayout, emitter) {
|
||||||
|
this.id = id;
|
||||||
|
this.emitter = emitter;
|
||||||
|
this.videoSpanId = `participant_${id}`;
|
||||||
this.VideoLayout = VideoLayout;
|
this.VideoLayout = VideoLayout;
|
||||||
this.addRemoteVideoContainer();
|
this.addRemoteVideoContainer();
|
||||||
this.connectionIndicator = new ConnectionIndicator(
|
this.connectionIndicator = new ConnectionIndicator(this, id);
|
||||||
this, this.peerJid);
|
|
||||||
this.setDisplayName();
|
this.setDisplayName();
|
||||||
var nickfield = document.createElement('span');
|
var nickfield = document.createElement('span');
|
||||||
nickfield.className = "nick";
|
nickfield.className = "nick";
|
||||||
nickfield.appendChild(document.createTextNode(this.resourceJid));
|
nickfield.appendChild(document.createTextNode(id));
|
||||||
this.container.appendChild(nickfield);
|
this.container.appendChild(nickfield);
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
this.flipX = false;
|
this.flipX = false;
|
||||||
this.isLocal = false;
|
this.isLocal = false;
|
||||||
|
SmallVideo.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
||||||
|
@ -30,18 +32,20 @@ RemoteVideo.prototype.constructor = RemoteVideo;
|
||||||
|
|
||||||
RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
||||||
this.container = RemoteVideo.createContainer(this.videoSpanId);
|
this.container = RemoteVideo.createContainer(this.videoSpanId);
|
||||||
if (APP.xmpp.isModerator())
|
if (APP.conference.isModerator) {
|
||||||
this.addRemoteVideoMenu();
|
this.addRemoteVideoMenu();
|
||||||
AudioLevels.updateAudioLevelCanvas(this.peerJid, this.VideoLayout);
|
}
|
||||||
|
let {thumbWidth, thumbHeight} = this.VideoLayout.calculateThumbnailSize();
|
||||||
|
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
|
||||||
|
|
||||||
return this.container;
|
return this.container;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the remote video menu element for the given <tt>jid</tt> in the
|
* Adds the remote video menu element for the given <tt>id</tt> in the
|
||||||
* given <tt>parentElement</tt>.
|
* given <tt>parentElement</tt>.
|
||||||
*
|
*
|
||||||
* @param jid the jid indicating the video for which we're adding a menu.
|
* @param id the id indicating the video for which we're adding a menu.
|
||||||
* @param parentElement the parent element where this menu will be added
|
* @param parentElement the parent element where this menu will be added
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -60,7 +64,7 @@ if (!interfaceConfig.filmStripOnly) {
|
||||||
|
|
||||||
var popupmenuElement = document.createElement('ul');
|
var popupmenuElement = document.createElement('ul');
|
||||||
popupmenuElement.className = 'popupmenu';
|
popupmenuElement.className = 'popupmenu';
|
||||||
popupmenuElement.id = 'remote_popupmenu_' + this.getResourceJid();
|
popupmenuElement.id = `remote_popupmenu_${this.id}`;
|
||||||
spanElement.appendChild(popupmenuElement);
|
spanElement.appendChild(popupmenuElement);
|
||||||
|
|
||||||
var muteMenuItem = document.createElement('li');
|
var muteMenuItem = document.createElement('li');
|
||||||
|
@ -88,7 +92,7 @@ if (!interfaceConfig.filmStripOnly) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
var isMute = !!self.isMuted;
|
var isMute = !!self.isMuted;
|
||||||
APP.xmpp.setMute(self.peerJid, !isMute);
|
self.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, self.id);
|
||||||
|
|
||||||
popupmenuElement.setAttribute('style', 'display:none;');
|
popupmenuElement.setAttribute('style', 'display:none;');
|
||||||
|
|
||||||
|
@ -117,7 +121,7 @@ if (!interfaceConfig.filmStripOnly) {
|
||||||
"data-i18n='videothumbnail.kick'> </div>";
|
"data-i18n='videothumbnail.kick'> </div>";
|
||||||
ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
|
ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
|
||||||
ejectLinkItem.onclick = function(){
|
ejectLinkItem.onclick = function(){
|
||||||
APP.xmpp.eject(self.peerJid);
|
self.emitter.emit(UIEvents.USER_KICKED, self.id);
|
||||||
popupmenuElement.setAttribute('style', 'display:none;');
|
popupmenuElement.setAttribute('style', 'display:none;');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -157,48 +161,49 @@ RemoteVideo.prototype.removeRemoteStreamElement =
|
||||||
select.remove();
|
select.remove();
|
||||||
|
|
||||||
console.info((isVideo ? "Video" : "Audio") +
|
console.info((isVideo ? "Video" : "Audio") +
|
||||||
" removed " + this.getResourceJid(), select);
|
" removed " + this.id, select);
|
||||||
|
|
||||||
if (isVideo)
|
if (isVideo)
|
||||||
this.VideoLayout.updateRemovedVideo(this.getResourceJid());
|
this.VideoLayout.updateRemovedVideo(this.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes RemoteVideo from the page.
|
* Removes RemoteVideo from the page.
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype.remove = function () {
|
RemoteVideo.prototype.remove = function () {
|
||||||
console.log("Remove thumbnail", this.peerJid);
|
console.log("Remove thumbnail", this.id);
|
||||||
this.removeConnectionIndicator();
|
this.removeConnectionIndicator();
|
||||||
// Make sure that the large video is updated if are removing its
|
// Make sure that the large video is updated if are removing its
|
||||||
// corresponding small video.
|
// corresponding small video.
|
||||||
this.VideoLayout.updateRemovedVideo(this.getResourceJid());
|
this.VideoLayout.updateRemovedVideo(this.id);
|
||||||
// Remove whole container
|
// Remove whole container
|
||||||
if (this.container.parentNode)
|
if (this.container.parentNode) {
|
||||||
this.container.parentNode.removeChild(this.container);
|
this.container.parentNode.removeChild(this.container);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
|
RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
|
||||||
|
|
||||||
var webRtcStream = stream.getOriginalStream();
|
var webRtcStream = stream.getOriginalStream();
|
||||||
var isVideo = stream.isVideoStream();
|
var isVideo = stream.isVideoTrack();
|
||||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var resourceJid = this.getResourceJid();
|
|
||||||
|
|
||||||
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
|
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
|
||||||
// when video playback starts
|
// when video playback starts
|
||||||
var onPlayingHandler = function () {
|
var onPlayingHandler = function () {
|
||||||
// FIXME: why do i have to do this for FF?
|
// FIXME: why do i have to do this for FF?
|
||||||
if (RTCBrowserType.isFirefox()) {
|
if (RTCBrowserType.isFirefox()) {
|
||||||
APP.RTC.attachMediaStream(sel, webRtcStream);
|
//FIXME: weshould use the lib here
|
||||||
|
//APP.RTC.attachMediaStream(sel, webRtcStream);
|
||||||
}
|
}
|
||||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||||
sel = self.selectVideoElement();
|
sel = self.selectVideoElement();
|
||||||
}
|
}
|
||||||
self.VideoLayout.videoactive(sel, resourceJid);
|
self.VideoLayout.videoactive(sel, self.id);
|
||||||
sel[0].onplaying = null;
|
sel[0].onplaying = null;
|
||||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||||
// 'currentTime' is used to check if the video has started
|
// 'currentTime' is used to check if the video has started
|
||||||
|
@ -210,39 +215,18 @@ RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||||
if (!this.container)
|
if (!this.container) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var self = this;
|
|
||||||
var webRtcStream = stream.getOriginalStream();
|
|
||||||
var isVideo = stream.isVideoStream();
|
|
||||||
var streamElement = SmallVideo.createStreamElement(stream);
|
|
||||||
var newElementId = streamElement.id;
|
|
||||||
|
|
||||||
// Put new stream element always in front
|
|
||||||
UIUtils.prependChild(this.container, streamElement);
|
|
||||||
|
|
||||||
var sel = $('#' + newElementId);
|
|
||||||
sel.hide();
|
|
||||||
|
|
||||||
// If the container is currently visible we attach the stream.
|
|
||||||
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
|
|
||||||
this.waitForPlayback(sel, stream);
|
|
||||||
|
|
||||||
APP.RTC.attachMediaStream(sel, webRtcStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
APP.RTC.addMediaStreamInactiveHandler(
|
this.stream = stream;
|
||||||
webRtcStream, function () {
|
|
||||||
console.log('stream ended', this);
|
|
||||||
|
|
||||||
self.removeRemoteStreamElement(webRtcStream, isVideo, newElementId);
|
let isVideo = stream.isVideoTrack();
|
||||||
});
|
|
||||||
|
|
||||||
// Add click handler.
|
// Add click handler.
|
||||||
var onClickHandler = function (event) {
|
let onClickHandler = (event) => {
|
||||||
|
|
||||||
self.VideoLayout.handleVideoThumbClicked(false, self.getResourceJid());
|
this.VideoLayout.handleVideoThumbClicked(false, this.id);
|
||||||
|
|
||||||
// On IE we need to populate this handler on video <object>
|
// On IE we need to populate this handler on video <object>
|
||||||
// and it does not give event instance as an argument,
|
// and it does not give event instance as an argument,
|
||||||
|
@ -254,14 +238,39 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
this.container.onclick = onClickHandler;
|
this.container.onclick = onClickHandler;
|
||||||
|
|
||||||
|
if(!stream.getOriginalStream())
|
||||||
|
return;
|
||||||
|
|
||||||
|
let streamElement = SmallVideo.createStreamElement(stream);
|
||||||
|
let newElementId = streamElement.id;
|
||||||
|
|
||||||
|
// Put new stream element always in front
|
||||||
|
UIUtils.prependChild(this.container, streamElement);
|
||||||
|
|
||||||
|
let sel = $(`#${newElementId}`);
|
||||||
|
|
||||||
|
// If the container is currently visible we attach the stream.
|
||||||
|
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
|
||||||
|
this.waitForPlayback(sel, stream);
|
||||||
|
|
||||||
|
stream.attach(sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide element only after stream was (maybe) attached
|
||||||
|
// because Temasys plugin requires video element
|
||||||
|
// to be visible to attach the stream
|
||||||
|
sel.hide();
|
||||||
|
|
||||||
// reselect
|
// reselect
|
||||||
if (RTCBrowserType.isTemasysPluginUsed())
|
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||||
sel = $('#' + newElementId);
|
sel = $(`#${newElementId}`);
|
||||||
sel[0].onclick = onClickHandler;
|
}
|
||||||
|
sel.click(onClickHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show/hide peer container for the given resourceJid.
|
* Show/hide peer container for the given id.
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype.showPeerContainer = function (state) {
|
RemoteVideo.prototype.showPeerContainer = function (state) {
|
||||||
if (!this.container)
|
if (!this.container)
|
||||||
|
@ -275,10 +284,10 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
|
||||||
resizeThumbnails = true;
|
resizeThumbnails = true;
|
||||||
$(this.container).show();
|
$(this.container).show();
|
||||||
}
|
}
|
||||||
// Call showAvatar with undefined, so that we'll figure out if avatar
|
// Call updateView, so that we'll figure out if avatar
|
||||||
// should be displayed based on video muted status and whether or not
|
// should be displayed based on video muted status and whether or not
|
||||||
// it's in the lastN set
|
// it's in the lastN set
|
||||||
this.showAvatar(undefined);
|
this.updateView();
|
||||||
}
|
}
|
||||||
else if ($(this.container).is(':visible') && isHide)
|
else if ($(this.container).is(':visible') && isHide)
|
||||||
{
|
{
|
||||||
|
@ -294,10 +303,16 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
|
||||||
|
|
||||||
// We want to be able to pin a participant from the contact list, even
|
// We want to be able to pin a participant from the contact list, even
|
||||||
// if he's not in the lastN set!
|
// if he's not in the lastN set!
|
||||||
// ContactList.setClickable(resourceJid, !isHide);
|
// ContactList.setClickable(id, !isHide);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RemoteVideo.prototype.updateResolution = function (resolution) {
|
||||||
|
if (this.connectionIndicator) {
|
||||||
|
this.connectionIndicator.updateResolution(resolution);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype.removeConnectionIndicator = function () {
|
RemoteVideo.prototype.removeConnectionIndicator = function () {
|
||||||
if (this.connectionIndicator)
|
if (this.connectionIndicator)
|
||||||
this.connectionIndicator.remove();
|
this.connectionIndicator.remove();
|
||||||
|
@ -311,12 +326,11 @@ RemoteVideo.prototype.hideConnectionIndicator = function () {
|
||||||
/**
|
/**
|
||||||
* Updates the remote video menu.
|
* Updates the remote video menu.
|
||||||
*
|
*
|
||||||
* @param jid the jid indicating the video for which we're adding a menu.
|
* @param id the id indicating the video for which we're adding a menu.
|
||||||
* @param isMuted indicates the current mute state
|
* @param isMuted indicates the current mute state
|
||||||
*/
|
*/
|
||||||
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
|
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
|
||||||
var muteMenuItem
|
var muteMenuItem = $(`#remote_popupmenu_${this.id}>li>a.mutelink`);
|
||||||
= $('#remote_popupmenu_' + this.getResourceJid() + '>li>a.mutelink');
|
|
||||||
|
|
||||||
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
|
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
|
||||||
|
|
||||||
|
@ -399,13 +413,11 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
|
||||||
nameSpan.className = 'displayname';
|
nameSpan.className = 'displayname';
|
||||||
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
|
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
|
||||||
|
|
||||||
|
|
||||||
if (displayName && displayName.length > 0) {
|
if (displayName && displayName.length > 0) {
|
||||||
nameSpan.innerText = displayName;
|
nameSpan.innerHTML = displayName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
nameSpan.innerHTML = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
||||||
|
|
||||||
nameSpan.id = this.videoSpanId + '_name';
|
nameSpan.id = this.videoSpanId + '_name';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -423,13 +435,6 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype.getResourceJid = function () {
|
|
||||||
if (!this.resourceJid) {
|
|
||||||
console.error("Undefined resource jid");
|
|
||||||
}
|
|
||||||
return this.resourceJid;
|
|
||||||
};
|
|
||||||
|
|
||||||
RemoteVideo.createContainer = function (spanId) {
|
RemoteVideo.createContainer = function (spanId) {
|
||||||
var container = document.createElement('span');
|
var container = document.createElement('span');
|
||||||
container.id = spanId;
|
container.id = spanId;
|
||||||
|
@ -439,4 +444,4 @@ RemoteVideo.createContainer = function (spanId) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = RemoteVideo;
|
export default RemoteVideo;
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
/* global $, APP, require */
|
/* global $, APP, require */
|
||||||
/* jshint -W101 */
|
/* jshint -W101 */
|
||||||
var Avatar = require("../avatar/Avatar");
|
import Avatar from "../avatar/Avatar";
|
||||||
var UIUtil = require("../util/UIUtil");
|
import UIUtil from "../util/UIUtil";
|
||||||
var LargeVideo = require("./LargeVideo");
|
|
||||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
|
||||||
|
|
||||||
function SmallVideo() {
|
function SmallVideo() {
|
||||||
this.isMuted = false;
|
this.isMuted = false;
|
||||||
this.hasAvatar = false;
|
this.hasAvatar = false;
|
||||||
|
this.isVideoMuted = false;
|
||||||
|
this.stream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility(selector, show) {
|
function setVisibility(selector, show) {
|
||||||
|
@ -17,6 +18,16 @@ function setVisibility(selector, show) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this small video is currently visible.
|
||||||
|
*
|
||||||
|
* @return <tt>true</tt> if this small video isn't currently visible and
|
||||||
|
* <tt>false</tt> - otherwise.
|
||||||
|
*/
|
||||||
|
SmallVideo.prototype.isVisible = function () {
|
||||||
|
return $('#' + this.videoSpanId).is(':visible');
|
||||||
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.showDisplayName = function(isShow) {
|
SmallVideo.prototype.showDisplayName = function(isShow) {
|
||||||
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
|
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
|
||||||
if (isShow) {
|
if (isShow) {
|
||||||
|
@ -57,7 +68,7 @@ SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the type of the video displayed by this instance.
|
* Sets the type of the video displayed by this instance.
|
||||||
* @param videoType 'camera' or 'screen'
|
* @param videoType 'camera' or 'desktop'
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.setVideoType = function (videoType) {
|
SmallVideo.prototype.setVideoType = function (videoType) {
|
||||||
this.videoType = videoType;
|
this.videoType = videoType;
|
||||||
|
@ -106,9 +117,10 @@ SmallVideo.prototype.setPresenceStatus = function (statusMsg) {
|
||||||
* Creates an audio or video element for a particular MediaStream.
|
* Creates an audio or video element for a particular MediaStream.
|
||||||
*/
|
*/
|
||||||
SmallVideo.createStreamElement = function (stream) {
|
SmallVideo.createStreamElement = function (stream) {
|
||||||
var isVideo = stream.isVideoStream();
|
let isVideo = stream.isVideoTrack();
|
||||||
|
|
||||||
var element = isVideo ? document.createElement('video')
|
let element = isVideo
|
||||||
|
? document.createElement('video')
|
||||||
: document.createElement('audio');
|
: document.createElement('audio');
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
element.setAttribute("muted", "true");
|
element.setAttribute("muted", "true");
|
||||||
|
@ -118,8 +130,7 @@ SmallVideo.createStreamElement = function (stream) {
|
||||||
element.autoplay = true;
|
element.autoplay = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') +
|
element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
|
||||||
APP.RTC.getStreamID(stream.getOriginalStream());
|
|
||||||
|
|
||||||
element.onplay = function () {
|
element.onplay = function () {
|
||||||
console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t",
|
console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t",
|
||||||
|
@ -144,8 +155,8 @@ SmallVideo.prototype.bindHoverHandler = function () {
|
||||||
function () {
|
function () {
|
||||||
// If the video has been "pinned" by the user we want to
|
// If the video has been "pinned" by the user we want to
|
||||||
// keep the display name on place.
|
// keep the display name on place.
|
||||||
if (!LargeVideo.isLargeVideoVisible() ||
|
if (!self.VideoLayout.isLargeVideoVisible() ||
|
||||||
!LargeVideo.isCurrentlyOnLarge(self.getResourceJid()))
|
!self.VideoLayout.isCurrentlyOnLarge(self.id))
|
||||||
self.showDisplayName(false);
|
self.showDisplayName(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -202,10 +213,12 @@ SmallVideo.prototype.showAudioIndicator = function(isMuted) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows video muted indicator over small videos.
|
* Shows video muted indicator over small videos and disables/enables avatar
|
||||||
|
* if video muted.
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.showVideoIndicator = function(isMuted) {
|
SmallVideo.prototype.setMutedView = function(isMuted) {
|
||||||
this.showAvatar(isMuted);
|
this.isVideoMuted = isMuted;
|
||||||
|
this.updateView();
|
||||||
|
|
||||||
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
|
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
|
||||||
|
|
||||||
|
@ -232,43 +245,9 @@ SmallVideo.prototype.showVideoIndicator = function(isMuted) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateIconPositions();
|
this.updateIconPositions();
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.enableDominantSpeaker = function (isEnable) {
|
|
||||||
var resourceJid = this.getResourceJid();
|
|
||||||
var displayName = resourceJid;
|
|
||||||
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
|
|
||||||
if (nameSpan.length > 0)
|
|
||||||
displayName = nameSpan.html();
|
|
||||||
|
|
||||||
console.log("UI enable dominant speaker",
|
|
||||||
displayName,
|
|
||||||
resourceJid,
|
|
||||||
isEnable);
|
|
||||||
|
|
||||||
|
|
||||||
if (!this.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEnable) {
|
|
||||||
this.showDisplayName(LargeVideo.getState() === "video");
|
|
||||||
|
|
||||||
if (!this.container.classList.contains("dominantspeaker"))
|
|
||||||
this.container.classList.add("dominantspeaker");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.showDisplayName(false);
|
|
||||||
|
|
||||||
if (this.container.classList.contains("dominantspeaker"))
|
|
||||||
this.container.classList.remove("dominantspeaker");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showAvatar();
|
|
||||||
};
|
|
||||||
|
|
||||||
SmallVideo.prototype.updateIconPositions = function () {
|
SmallVideo.prototype.updateIconPositions = function () {
|
||||||
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
|
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
|
||||||
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
|
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
|
||||||
|
@ -316,13 +295,15 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.selectVideoElement = function () {
|
SmallVideo.prototype.selectVideoElement = function () {
|
||||||
var videoElem = APP.RTC.getVideoElementName();
|
var videoElemName;
|
||||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
||||||
return $('#' + this.videoSpanId).find(videoElem);
|
videoElemName = 'video';
|
||||||
|
return $('#' + this.videoSpanId).find(videoElemName);
|
||||||
} else {
|
} else {
|
||||||
|
videoElemName = 'object';
|
||||||
var matching = $('#' + this.videoSpanId +
|
var matching = $('#' + this.videoSpanId +
|
||||||
(this.isLocal ? '>>' : '>') +
|
(this.isLocal ? '>>' : '>') +
|
||||||
videoElem + '>param[value="video"]');
|
videoElemName + '>param[value="video"]');
|
||||||
if (matching.length < 2) {
|
if (matching.length < 2) {
|
||||||
return matching.parent();
|
return matching.parent();
|
||||||
}
|
}
|
||||||
|
@ -343,11 +324,6 @@ SmallVideo.prototype.selectVideoElement = function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.getSrc = function () {
|
|
||||||
var videoElement = this.selectVideoElement().get(0);
|
|
||||||
return APP.RTC.getVideoSrc(videoElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
SmallVideo.prototype.focus = function(isFocused) {
|
SmallVideo.prototype.focus = function(isFocused) {
|
||||||
if(!isFocused) {
|
if(!isFocused) {
|
||||||
this.container.classList.remove("videoContainerFocused");
|
this.container.classList.remove("videoContainerFocused");
|
||||||
|
@ -361,66 +337,76 @@ SmallVideo.prototype.hasVideo = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides or shows the user's avatar
|
* Hides or shows the user's avatar.
|
||||||
|
* This update assumes that large video had been updated and we will
|
||||||
|
* reflect it on this small video.
|
||||||
|
*
|
||||||
* @param show whether we should show the avatar or not
|
* @param show whether we should show the avatar or not
|
||||||
* video because there is no dominant speaker and no focused speaker
|
* video because there is no dominant speaker and no focused speaker
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.showAvatar = function (show) {
|
SmallVideo.prototype.updateView = function () {
|
||||||
if (!this.hasAvatar) {
|
if (!this.hasAvatar) {
|
||||||
if (this.peerJid) {
|
if (this.id) {
|
||||||
// Init avatar
|
// Init avatar
|
||||||
this.avatarChanged(Avatar.getAvatarUrl(this.peerJid));
|
this.avatarChanged(Avatar.getAvatarUrl(this.id));
|
||||||
} else {
|
} else {
|
||||||
console.error("Unable to init avatar - no peerjid", this);
|
console.error("Unable to init avatar - no id", this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourceJid = this.getResourceJid();
|
let video = this.selectVideoElement();
|
||||||
var video = this.selectVideoElement();
|
|
||||||
|
|
||||||
var avatar = $('#avatar_' + resourceJid);
|
let avatar = $(`#avatar_${this.id}`);
|
||||||
|
|
||||||
if (show === undefined || show === null) {
|
var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
|
||||||
if (!this.isLocal &&
|
|
||||||
!this.VideoLayout.isInLastN(resourceJid)) {
|
var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
|
||||||
show = true;
|
var showAvatar;
|
||||||
|
if ((!this.isLocal
|
||||||
|
&& !this.VideoLayout.isInLastN(this.id))
|
||||||
|
|| this.isVideoMuted) {
|
||||||
|
showAvatar = true;
|
||||||
} else {
|
} else {
|
||||||
// We want to show the avatar when the video is muted or not exists
|
// We want to show the avatar when the video is muted or not exists
|
||||||
// that is when 'true' or 'null' is returned
|
// that is when 'true' or 'null' is returned
|
||||||
show = APP.RTC.isVideoMuted(this.peerJid) !== false;
|
showAvatar = !this.stream || this.stream.isMuted();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LargeVideo.showAvatar(resourceJid, show)) {
|
showAvatar = showAvatar && !isCurrentlyOnLarge;
|
||||||
setVisibility(avatar, false);
|
|
||||||
setVisibility(video, false);
|
|
||||||
} else {
|
|
||||||
if (video && video.length > 0) {
|
if (video && video.length > 0) {
|
||||||
setVisibility(video, !show);
|
setVisibility(video, showVideo);
|
||||||
}
|
}
|
||||||
setVisibility(avatar, show);
|
setVisibility(avatar, showAvatar);
|
||||||
|
|
||||||
|
var showDisplayName = !showVideo && !showAvatar;
|
||||||
|
|
||||||
|
if (showDisplayName) {
|
||||||
|
this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.showDisplayName(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.avatarChanged = function (thumbUrl) {
|
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
|
||||||
var thumbnail = $('#' + this.videoSpanId);
|
var thumbnail = $('#' + this.videoSpanId);
|
||||||
var resourceJid = this.getResourceJid();
|
var avatar = $('#avatar_' + this.id);
|
||||||
var avatar = $('#avatar_' + resourceJid);
|
|
||||||
this.hasAvatar = true;
|
this.hasAvatar = true;
|
||||||
|
|
||||||
// set the avatar in the thumbnail
|
// set the avatar in the thumbnail
|
||||||
if (avatar && avatar.length > 0) {
|
if (avatar && avatar.length > 0) {
|
||||||
avatar[0].src = thumbUrl;
|
avatar[0].src = avatarUrl;
|
||||||
} else {
|
} else {
|
||||||
if (thumbnail && thumbnail.length > 0) {
|
if (thumbnail && thumbnail.length > 0) {
|
||||||
avatar = document.createElement('img');
|
avatar = document.createElement('img');
|
||||||
avatar.id = 'avatar_' + resourceJid;
|
avatar.id = 'avatar_' + this.id;
|
||||||
avatar.className = 'userAvatar';
|
avatar.className = 'userAvatar';
|
||||||
avatar.src = thumbUrl;
|
avatar.src = avatarUrl;
|
||||||
thumbnail.append(avatar);
|
thumbnail.append(avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = SmallVideo;
|
export default SmallVideo;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
||||||
var jssha = require('jssha');
|
var JSSHA = require('jssha');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +16,7 @@ module.exports = {
|
||||||
// This implements the actual choice of an entry in the list based on
|
// This implements the actual choice of an entry in the list based on
|
||||||
// roomName. Please consider the implications for existing deployments
|
// roomName. Please consider the implications for existing deployments
|
||||||
// before introducing changes.
|
// before introducing changes.
|
||||||
var hash = (new jssha(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
|
var hash = (new JSSHA(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
|
||||||
var n = parseInt("0x"+hash.substr(-6));
|
var n = parseInt("0x"+hash.substr(-6));
|
||||||
var idx = n % config.boshList.length;
|
var idx = n % config.boshList.length;
|
||||||
var attemptFirstAddress;
|
var attemptFirstAddress;
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
var EventEmitter = require("events");
|
var EventEmitter = require("events");
|
||||||
var eventEmitter = new EventEmitter();
|
var eventEmitter = new EventEmitter();
|
||||||
var CQEvents = require("../../service/connectionquality/CQEvents");
|
var CQEvents = require("../../service/connectionquality/CQEvents");
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var StatisticsEvents = require("../../service/statistics/Events");
|
var StatisticsEvents = require("../../service/statistics/Events");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,69 +17,47 @@ var stats = {};
|
||||||
*/
|
*/
|
||||||
var remoteStats = {};
|
var remoteStats = {};
|
||||||
|
|
||||||
/**
|
|
||||||
* Interval for sending statistics to other participants
|
|
||||||
* @type {null}
|
|
||||||
*/
|
|
||||||
var sendIntervalId = null;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start statistics sending.
|
|
||||||
*/
|
|
||||||
function startSendingStats() {
|
|
||||||
sendStats();
|
|
||||||
sendIntervalId = setInterval(sendStats, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends statistics to other participants
|
|
||||||
*/
|
|
||||||
function sendStats() {
|
|
||||||
APP.xmpp.addToPresence("connectionQuality", convertToMUCStats(stats));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts statistics to format for sending through XMPP
|
|
||||||
* @param stats the statistics
|
|
||||||
* @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}}
|
|
||||||
*/
|
|
||||||
function convertToMUCStats(stats) {
|
|
||||||
return {
|
|
||||||
"bitrate_download": stats.bitrate.download,
|
|
||||||
"bitrate_upload": stats.bitrate.upload,
|
|
||||||
"packetLoss_total": stats.packetLoss.total,
|
|
||||||
"packetLoss_download": stats.packetLoss.download,
|
|
||||||
"packetLoss_upload": stats.packetLoss.upload
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts statistics to format used by VideoLayout
|
* Converts statistics to format used by VideoLayout
|
||||||
* @param stats
|
* @param stats
|
||||||
* @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
|
* @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
|
||||||
*/
|
*/
|
||||||
function parseMUCStats(stats) {
|
function parseMUCStats(stats) {
|
||||||
|
if(!stats || !stats.children || !stats.children.length)
|
||||||
|
return null;
|
||||||
|
var children = stats.children;
|
||||||
|
var extractedStats = {};
|
||||||
|
children.forEach((child) => {
|
||||||
|
if(child.tagName !== "stat" || !child.attributes)
|
||||||
|
return;
|
||||||
|
var attrKeys = Object.keys(child.attributes);
|
||||||
|
if(!attrKeys || !attrKeys.length)
|
||||||
|
return;
|
||||||
|
attrKeys.forEach((attr) => {
|
||||||
|
extractedStats[attr] = child.attributes[attr];
|
||||||
|
});
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
bitrate: {
|
bitrate: {
|
||||||
download: stats.bitrate_download,
|
download: extractedStats.bitrate_download,
|
||||||
upload: stats.bitrate_upload
|
upload: extractedStats.bitrate_upload
|
||||||
},
|
},
|
||||||
packetLoss: {
|
packetLoss: {
|
||||||
total: stats.packetLoss_total,
|
total: extractedStats.packetLoss_total,
|
||||||
download: stats.packetLoss_download,
|
download: extractedStats.packetLoss_download,
|
||||||
upload: stats.packetLoss_upload
|
upload: extractedStats.packetLoss_upload
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConnectionQuality = {
|
var ConnectionQuality = {
|
||||||
init: function () {
|
init: function () {
|
||||||
APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats);
|
APP.statistics.addListener(
|
||||||
APP.statistics.addListener(StatisticsEvents.CONNECTION_STATS,
|
StatisticsEvents.CONNECTION_STATS, this.updateLocalStats
|
||||||
this.updateLocalStats);
|
);
|
||||||
APP.statistics.addListener(StatisticsEvents.STOP,
|
APP.statistics.addListener(
|
||||||
this.stopSendingStats);
|
StatisticsEvents.STOP, this.stopSendingStats
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,33 +67,30 @@ var ConnectionQuality = {
|
||||||
updateLocalStats: function (data) {
|
updateLocalStats: function (data) {
|
||||||
stats = data;
|
stats = data;
|
||||||
eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
|
eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
|
||||||
if (!sendIntervalId) {
|
|
||||||
startSendingStats();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates remote statistics
|
* Updates remote statistics
|
||||||
* @param jid the jid associated with the statistics
|
* @param id the id associated with the statistics
|
||||||
* @param data the statistics
|
* @param data the statistics
|
||||||
*/
|
*/
|
||||||
updateRemoteStats: function (jid, data) {
|
updateRemoteStats: function (id, data) {
|
||||||
if (!data || !data.packetLoss_total) {
|
data = parseMUCStats(data);
|
||||||
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null);
|
if (!data || !data.packetLoss || !data.packetLoss.total) {
|
||||||
|
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, id, null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
remoteStats[jid] = parseMUCStats(data);
|
remoteStats[id] = data;
|
||||||
|
|
||||||
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED,
|
eventEmitter.emit(
|
||||||
jid, 100 - data.packetLoss_total, remoteStats[jid]);
|
CQEvents.REMOTESTATS_UPDATED, id, 100 - data.packetLoss_total, remoteStats[id]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops statistics sending.
|
* Stops statistics sending.
|
||||||
*/
|
*/
|
||||||
stopSendingStats: function () {
|
stopSendingStats: function () {
|
||||||
clearInterval(sendIntervalId);
|
|
||||||
sendIntervalId = null;
|
|
||||||
//notify UI about stopping statistics gathering
|
//notify UI about stopping statistics gathering
|
||||||
eventEmitter.emit(CQEvents.STOP);
|
eventEmitter.emit(CQEvents.STOP);
|
||||||
},
|
},
|
||||||
|
@ -130,8 +104,26 @@ var ConnectionQuality = {
|
||||||
|
|
||||||
addListener: function (type, listener) {
|
addListener: function (type, listener) {
|
||||||
eventEmitter.on(type, listener);
|
eventEmitter.on(type, listener);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts statistics to format for sending through XMPP
|
||||||
|
* @param stats the statistics
|
||||||
|
* @returns [{tagName: "stat", attributes: {{bitrate_donwload: *}},
|
||||||
|
* {tagName: "stat", attributes: {{ bitrate_uplpoad: *}},
|
||||||
|
* {tagName: "stat", attributes: {{ packetLoss_total: *}},
|
||||||
|
* {tagName: "stat", attributes: {{ packetLoss_download: *}},
|
||||||
|
* {tagName: "stat", attributes: {{ packetLoss_upload: *}}]
|
||||||
|
*/
|
||||||
|
convertToMUCStats: function (stats) {
|
||||||
|
return [
|
||||||
|
{tagName: "stat", attributes: {"bitrate_download": stats.bitrate.download}},
|
||||||
|
{tagName: "stat", attributes: {"bitrate_upload": stats.bitrate.upload}},
|
||||||
|
{tagName: "stat", attributes: {"packetLoss_total": stats.packetLoss.total}},
|
||||||
|
{tagName: "stat", attributes: {"packetLoss_download": stats.packetLoss.download}},
|
||||||
|
{tagName: "stat", attributes: {"packetLoss_upload": stats.packetLoss.upload}}
|
||||||
|
];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ConnectionQuality;
|
module.exports = ConnectionQuality;
|
|
@ -1,407 +0,0 @@
|
||||||
/* global config, APP, chrome, $, alert */
|
|
||||||
/* jshint -W003 */
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
var AdapterJS = require("../RTC/adapter.screenshare");
|
|
||||||
var DesktopSharingEventTypes
|
|
||||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the Chrome desktop sharing extension is installed.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
var chromeExtInstalled = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether an update of the Chrome desktop sharing extension is
|
|
||||||
* required.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
var chromeExtUpdateRequired = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the jidesha extension for firefox is installed for the domain on
|
|
||||||
* which we are running. Null designates an unknown value.
|
|
||||||
* @type {null}
|
|
||||||
*/
|
|
||||||
var firefoxExtInstalled = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set to true, detection of an installed firefox extension will be started
|
|
||||||
* again the next time obtainScreenOnFirefox is called (e.g. next time the
|
|
||||||
* user tries to enable screen sharing).
|
|
||||||
*/
|
|
||||||
var reDetectFirefoxExtension = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles obtaining a stream from a screen capture on different browsers.
|
|
||||||
*/
|
|
||||||
function ScreenObtainer(){
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The EventEmitter to use to emit events.
|
|
||||||
* @type {null}
|
|
||||||
*/
|
|
||||||
ScreenObtainer.prototype.eventEmitter = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the function used to obtain a screen capture (this.obtainStream).
|
|
||||||
*
|
|
||||||
* If the browser is Chrome, it uses the value of
|
|
||||||
* 'config.desktopSharingChromeMethod' (or 'config.desktopSharing') to * decide
|
|
||||||
* whether to use the a Chrome extension (if the value is 'ext'), use the
|
|
||||||
* "screen" media source (if the value is 'webrtc'), or disable screen capture
|
|
||||||
* (if the value is other).
|
|
||||||
* Note that for the "screen" media source to work the
|
|
||||||
* 'chrome://flags/#enable-usermedia-screen-capture' flag must be set.
|
|
||||||
*/
|
|
||||||
ScreenObtainer.prototype.init = function(eventEmitter) {
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
var obtainDesktopStream = null;
|
|
||||||
|
|
||||||
if (RTCBrowserType.isFirefox())
|
|
||||||
initFirefoxExtensionDetection();
|
|
||||||
|
|
||||||
// TODO remove this, config.desktopSharing is deprecated.
|
|
||||||
var chromeMethod =
|
|
||||||
(config.desktopSharingChromeMethod || config.desktopSharing);
|
|
||||||
|
|
||||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
|
|
||||||
console.info("Screensharing not supported by this plugin version");
|
|
||||||
} else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
|
|
||||||
console.info(
|
|
||||||
"Screensharing not available with Temasys plugin on this site");
|
|
||||||
} else {
|
|
||||||
obtainDesktopStream = obtainWebRTCScreen;
|
|
||||||
console.info("Using Temasys plugin for desktop sharing");
|
|
||||||
}
|
|
||||||
} else if (RTCBrowserType.isChrome()) {
|
|
||||||
if (chromeMethod == "ext") {
|
|
||||||
if (RTCBrowserType.getChromeVersion() >= 34) {
|
|
||||||
obtainDesktopStream = obtainScreenFromExtension;
|
|
||||||
console.info("Using Chrome extension for desktop sharing");
|
|
||||||
initChromeExtension();
|
|
||||||
} else {
|
|
||||||
console.info("Chrome extension not supported until ver 34");
|
|
||||||
}
|
|
||||||
} else if (chromeMethod == "webrtc") {
|
|
||||||
obtainDesktopStream = obtainWebRTCScreen;
|
|
||||||
console.info("Using Chrome WebRTC for desktop sharing");
|
|
||||||
}
|
|
||||||
} else if (RTCBrowserType.isFirefox()) {
|
|
||||||
if (config.desktopSharingFirefoxDisabled) {
|
|
||||||
obtainDesktopStream = null;
|
|
||||||
} else if (window.location.protocol === "http:"){
|
|
||||||
console.log("Screen sharing is not supported over HTTP. Use of " +
|
|
||||||
"HTTPS is required.");
|
|
||||||
obtainDesktopStream = null;
|
|
||||||
} else {
|
|
||||||
obtainDesktopStream = this.obtainScreenOnFirefox;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!obtainDesktopStream) {
|
|
||||||
console.info("Desktop sharing disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
ScreenObtainer.prototype.obtainStream = obtainDesktopStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
ScreenObtainer.prototype.obtainStream = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether obtaining a screen capture is supported in the current
|
|
||||||
* environment.
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
ScreenObtainer.prototype.isSupported = function() {
|
|
||||||
return !!this.obtainStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains a desktop stream using getUserMedia.
|
|
||||||
* For this to work on Chrome, the
|
|
||||||
* 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled.
|
|
||||||
*
|
|
||||||
* On firefox, the document's domain must be white-listed in the
|
|
||||||
* 'media.getusermedia.screensharing.allowed_domains' preference in
|
|
||||||
* 'about:config'.
|
|
||||||
*/
|
|
||||||
function obtainWebRTCScreen(streamCallback, failCallback) {
|
|
||||||
APP.RTC.getUserMediaWithConstraints(
|
|
||||||
['screen'],
|
|
||||||
streamCallback,
|
|
||||||
failCallback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs inline install URL for Chrome desktop streaming extension.
|
|
||||||
* The 'chromeExtensionId' must be defined in config.js.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getWebStoreInstallUrl()
|
|
||||||
{
|
|
||||||
//TODO remove chromeExtensionId (deprecated)
|
|
||||||
return "https://chrome.google.com/webstore/detail/" +
|
|
||||||
(config.desktopSharingChromeExtId || config.chromeExtensionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether an update of the Chrome extension is required.
|
|
||||||
* @param minVersion minimal required version
|
|
||||||
* @param extVersion current extension version
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function isUpdateRequired(minVersion, extVersion) {
|
|
||||||
try {
|
|
||||||
var s1 = minVersion.split('.');
|
|
||||||
var s2 = extVersion.split('.');
|
|
||||||
|
|
||||||
var len = Math.max(s1.length, s2.length);
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
var n1 = 0,
|
|
||||||
n2 = 0;
|
|
||||||
|
|
||||||
if (i < s1.length)
|
|
||||||
n1 = parseInt(s1[i]);
|
|
||||||
if (i < s2.length)
|
|
||||||
n2 = parseInt(s2[i]);
|
|
||||||
|
|
||||||
if (isNaN(n1) || isNaN(n2)) {
|
|
||||||
return true;
|
|
||||||
} else if (n1 !== n2) {
|
|
||||||
return n1 > n2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// will happen if both versions have identical numbers in
|
|
||||||
// their components (even if one of them is longer, has more components)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error("Failed to parse extension version", e);
|
|
||||||
APP.UI.messageHandler.showError("dialog.error",
|
|
||||||
"dialog.detectext");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkChromeExtInstalled(callback) {
|
|
||||||
if (!chrome || !chrome.runtime) {
|
|
||||||
// No API, so no extension for sure
|
|
||||||
callback(false, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.runtime.sendMessage(
|
|
||||||
//TODO: remove chromeExtensionId (deprecated)
|
|
||||||
(config.desktopSharingChromeExtId || config.chromeExtensionId),
|
|
||||||
{ getVersion: true },
|
|
||||||
function (response) {
|
|
||||||
if (!response || !response.version) {
|
|
||||||
// Communication failure - assume that no endpoint exists
|
|
||||||
console.warn(
|
|
||||||
"Extension not installed?: ", chrome.runtime.lastError);
|
|
||||||
callback(false, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check installed extension version
|
|
||||||
var extVersion = response.version;
|
|
||||||
console.log('Extension version is: ' + extVersion);
|
|
||||||
//TODO: remove minChromeExtVersion (deprecated)
|
|
||||||
var updateRequired
|
|
||||||
= isUpdateRequired(
|
|
||||||
(config.desktopSharingChromeMinExtVersion ||
|
|
||||||
config.minChromeExtVersion),
|
|
||||||
extVersion);
|
|
||||||
callback(!updateRequired, updateRequired);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function doGetStreamFromExtension(streamCallback, failCallback) {
|
|
||||||
// Sends 'getStream' msg to the extension.
|
|
||||||
// Extension id must be defined in the config.
|
|
||||||
chrome.runtime.sendMessage(
|
|
||||||
//TODO: remove chromeExtensionId (deprecated)
|
|
||||||
(config.desktopSharingChromeExtId || config.chromeExtensionId),
|
|
||||||
{
|
|
||||||
getStream: true,
|
|
||||||
//TODO: remove desktopSharingSources (deprecated).
|
|
||||||
sources: (config.desktopSharingChromeSources ||
|
|
||||||
config.desktopSharingSources)
|
|
||||||
},
|
|
||||||
function (response) {
|
|
||||||
if (!response) {
|
|
||||||
failCallback(chrome.runtime.lastError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("Response from extension: " + response);
|
|
||||||
if (response.streamId) {
|
|
||||||
APP.RTC.getUserMediaWithConstraints(
|
|
||||||
['desktop'],
|
|
||||||
function (stream) {
|
|
||||||
streamCallback(stream);
|
|
||||||
},
|
|
||||||
failCallback,
|
|
||||||
null, null, null,
|
|
||||||
response.streamId);
|
|
||||||
} else {
|
|
||||||
failCallback("Extension failed to get the stream");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop'
|
|
||||||
* stream for returned stream token.
|
|
||||||
*/
|
|
||||||
function obtainScreenFromExtension(streamCallback, failCallback) {
|
|
||||||
if (chromeExtInstalled) {
|
|
||||||
doGetStreamFromExtension(streamCallback, failCallback);
|
|
||||||
} else {
|
|
||||||
if (chromeExtUpdateRequired) {
|
|
||||||
alert(
|
|
||||||
'Jitsi Desktop Streamer requires update. ' +
|
|
||||||
'Changes will take effect after next Chrome restart.');
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.webstore.install(
|
|
||||||
getWebStoreInstallUrl(),
|
|
||||||
function (arg) {
|
|
||||||
console.log("Extension installed successfully", arg);
|
|
||||||
chromeExtInstalled = true;
|
|
||||||
// We need to give a moment for the endpoint to become available
|
|
||||||
window.setTimeout(function () {
|
|
||||||
doGetStreamFromExtension(streamCallback, failCallback);
|
|
||||||
}, 500);
|
|
||||||
},
|
|
||||||
function (arg) {
|
|
||||||
console.log("Failed to install the extension", arg);
|
|
||||||
failCallback(arg);
|
|
||||||
APP.UI.messageHandler.showError("dialog.error",
|
|
||||||
"dialog.failtoinstall");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes <link rel=chrome-webstore-item /> with extension id set in
|
|
||||||
* config.js to support inline installs. Host site must be selected as main
|
|
||||||
* website of published extension.
|
|
||||||
*/
|
|
||||||
function initInlineInstalls()
|
|
||||||
{
|
|
||||||
$("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
function initChromeExtension() {
|
|
||||||
// Initialize Chrome extension inline installs
|
|
||||||
initInlineInstalls();
|
|
||||||
// Check if extension is installed
|
|
||||||
checkChromeExtInstalled(function (installed, updateRequired) {
|
|
||||||
chromeExtInstalled = installed;
|
|
||||||
chromeExtUpdateRequired = updateRequired;
|
|
||||||
console.info(
|
|
||||||
"Chrome extension installed: " + chromeExtInstalled +
|
|
||||||
" updateRequired: " + chromeExtUpdateRequired);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains a screen capture stream on Firefox.
|
|
||||||
* @param callback
|
|
||||||
* @param errorCallback
|
|
||||||
*/
|
|
||||||
ScreenObtainer.prototype.obtainScreenOnFirefox =
|
|
||||||
function (callback, errorCallback) {
|
|
||||||
var self = this;
|
|
||||||
var extensionRequired = false;
|
|
||||||
if (config.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
|
|
||||||
(config.desktopSharingFirefoxMaxVersionExtRequired >= 0 &&
|
|
||||||
RTCBrowserType.getFirefoxVersion() <=
|
|
||||||
config.desktopSharingFirefoxMaxVersionExtRequired)) {
|
|
||||||
extensionRequired = true;
|
|
||||||
console.log("Jidesha extension required on firefox version " +
|
|
||||||
RTCBrowserType.getFirefoxVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!extensionRequired || firefoxExtInstalled === true) {
|
|
||||||
obtainWebRTCScreen(callback, errorCallback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reDetectFirefoxExtension) {
|
|
||||||
reDetectFirefoxExtension = false;
|
|
||||||
initFirefoxExtensionDetection();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give it some (more) time to initialize, and assume lack of extension if
|
|
||||||
// it hasn't.
|
|
||||||
if (firefoxExtInstalled === null) {
|
|
||||||
window.setTimeout(
|
|
||||||
function() {
|
|
||||||
if (firefoxExtInstalled === null)
|
|
||||||
firefoxExtInstalled = false;
|
|
||||||
self.obtainScreenOnFirefox(callback, errorCallback);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
console.log("Waiting for detection of jidesha on firefox to finish.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need an extension and it isn't installed.
|
|
||||||
|
|
||||||
// Make sure we check for the extension when the user clicks again.
|
|
||||||
firefoxExtInstalled = null;
|
|
||||||
reDetectFirefoxExtension = true;
|
|
||||||
|
|
||||||
// Prompt the user to install the extension
|
|
||||||
this.eventEmitter.emit(DesktopSharingEventTypes.FIREFOX_EXTENSION_NEEDED,
|
|
||||||
config.desktopSharingFirefoxExtensionURL);
|
|
||||||
|
|
||||||
// Make sure desktopsharing knows that we failed, so that it doesn't get
|
|
||||||
// stuck in 'switching' mode.
|
|
||||||
errorCallback('Firefox extension required.');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the detection of an installed jidesha extension for firefox.
|
|
||||||
*/
|
|
||||||
function initFirefoxExtensionDetection() {
|
|
||||||
if (config.desktopSharingFirefoxDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (firefoxExtInstalled === false || firefoxExtInstalled === true)
|
|
||||||
return;
|
|
||||||
if (!config.desktopSharingFirefoxExtId) {
|
|
||||||
firefoxExtInstalled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.onload = function(){
|
|
||||||
console.log("Detected firefox screen sharing extension.");
|
|
||||||
firefoxExtInstalled = true;
|
|
||||||
};
|
|
||||||
img.onerror = function(){
|
|
||||||
console.log("Detected lack of firefox screen sharing extension.");
|
|
||||||
firefoxExtInstalled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The jidesha extension exposes an empty image file under the url:
|
|
||||||
// "chrome://EXT_ID/content/DOMAIN.png"
|
|
||||||
// Where EXT_ID is the ID of the extension with "@" replaced by ".", and
|
|
||||||
// DOMAIN is a domain whitelisted by the extension.
|
|
||||||
var src = "chrome://" +
|
|
||||||
(config.desktopSharingFirefoxExtId.replace('@', '.')) +
|
|
||||||
"/content/" + document.location.hostname + ".png";
|
|
||||||
img.setAttribute('src', src);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ScreenObtainer;
|
|
|
@ -1,10 +1,8 @@
|
||||||
/* global APP, config */
|
/* global APP, JitsiMeetJS, config */
|
||||||
var EventEmitter = require("events");
|
var EventEmitter = require("events");
|
||||||
var DesktopSharingEventTypes
|
import DSEvents from '../../service/desktopsharing/DesktopSharingEventTypes';
|
||||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
const TrackEvents = JitsiMeetJS.events.track;
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var ScreenObtainer = require("./ScreenObtainer");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that desktop stream is currently in use (for toggle purpose).
|
* Indicates that desktop stream is currently in use (for toggle purpose).
|
||||||
|
@ -20,22 +18,19 @@ var isUsingScreenStream = false;
|
||||||
var switchInProgress = false;
|
var switchInProgress = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to obtain the screen sharing stream from the browser.
|
* true if desktop sharing is enabled and false otherwise.
|
||||||
*/
|
*/
|
||||||
var screenObtainer = new ScreenObtainer();
|
var isEnabled = false;
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
var eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
function streamSwitchDone() {
|
function streamSwitchDone() {
|
||||||
switchInProgress = false;
|
switchInProgress = false;
|
||||||
eventEmitter.emit(
|
eventEmitter.emit(DSEvents.SWITCHING_DONE, isUsingScreenStream);
|
||||||
DesktopSharingEventTypes.SWITCHING_DONE,
|
|
||||||
isUsingScreenStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function newStreamCreated(stream) {
|
function newStreamCreated(track) {
|
||||||
eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
eventEmitter.emit(DSEvents.NEW_STREAM_CREATED, track, streamSwitchDone);
|
||||||
stream, isUsingScreenStream, streamSwitchDone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoStreamFailed(error) {
|
function getVideoStreamFailed(error) {
|
||||||
|
@ -50,36 +45,31 @@ function getDesktopStreamFailed(error) {
|
||||||
switchInProgress = false;
|
switchInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEndedHandler(stream) {
|
function onEndedHandler() {
|
||||||
if (!switchInProgress && isUsingScreenStream) {
|
if (!switchInProgress && isUsingScreenStream) {
|
||||||
APP.desktopsharing.toggleScreenSharing();
|
APP.desktopsharing.toggleScreenSharing();
|
||||||
}
|
}
|
||||||
|
|
||||||
APP.RTC.removeMediaStreamInactiveHandler(stream, onEndedHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isUsingScreenStream: function () {
|
isUsingScreenStream: function () {
|
||||||
return isUsingScreenStream;
|
return isUsingScreenStream;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Initializes the desktop sharing module.
|
||||||
|
* @param {boolean} <tt>true</tt> if desktop sharing feature is available
|
||||||
|
* and enabled.
|
||||||
|
*/
|
||||||
|
init: function (enabled) {
|
||||||
|
isEnabled = enabled;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available
|
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available
|
||||||
* and enabled.
|
* and enabled.
|
||||||
*/
|
*/
|
||||||
isDesktopSharingEnabled: function () {
|
isDesktopSharingEnabled: function () {
|
||||||
return screenObtainer.isSupported();
|
return isEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function () {
|
|
||||||
// Called when RTC finishes initialization
|
|
||||||
APP.RTC.addListener(RTCEvents.RTC_READY,
|
|
||||||
function() {
|
|
||||||
screenObtainer.init(eventEmitter);
|
|
||||||
eventEmitter.emit(DesktopSharingEventTypes.INIT);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addListener: function (type, listener) {
|
addListener: function (type, listener) {
|
||||||
eventEmitter.on(type, listener);
|
eventEmitter.on(type, listener);
|
||||||
},
|
},
|
||||||
|
@ -95,43 +85,50 @@ module.exports = {
|
||||||
if (switchInProgress) {
|
if (switchInProgress) {
|
||||||
console.warn("Switch in progress.");
|
console.warn("Switch in progress.");
|
||||||
return;
|
return;
|
||||||
} else if (!screenObtainer.isSupported()) {
|
} else if (!this.isDesktopSharingEnabled()) {
|
||||||
console.warn("Cannot toggle screen sharing: not supported.");
|
console.warn("Cannot toggle screen sharing: not supported.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switchInProgress = true;
|
switchInProgress = true;
|
||||||
|
let type;
|
||||||
if (!isUsingScreenStream) {
|
if (!isUsingScreenStream) {
|
||||||
// Switch to desktop stream
|
// Switch to desktop stream
|
||||||
screenObtainer.obtainStream(
|
type = "desktop";
|
||||||
function (stream) {
|
|
||||||
// We now use screen stream
|
|
||||||
isUsingScreenStream = true;
|
|
||||||
// Hook 'ended' event to restore camera
|
|
||||||
// when screen stream stops
|
|
||||||
APP.RTC.addMediaStreamInactiveHandler(
|
|
||||||
stream, onEndedHandler);
|
|
||||||
newStreamCreated(stream);
|
|
||||||
},
|
|
||||||
getDesktopStreamFailed);
|
|
||||||
} else {
|
} else {
|
||||||
// Disable screen stream
|
type = "video";
|
||||||
APP.RTC.getUserMediaWithConstraints(
|
|
||||||
['video'],
|
|
||||||
function (stream) {
|
|
||||||
// We are now using camera stream
|
|
||||||
isUsingScreenStream = false;
|
|
||||||
newStreamCreated(stream);
|
|
||||||
},
|
|
||||||
getVideoStreamFailed,
|
|
||||||
config.resolution || '360'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
var fail = (error) => {
|
||||||
/*
|
if (type === 'desktop') {
|
||||||
* Exports the event emitter to allow use by ScreenObtainer. Not for outside
|
getDesktopStreamFailed(error);
|
||||||
* use.
|
} else {
|
||||||
*/
|
getVideoStreamFailed(error);
|
||||||
eventEmitter: eventEmitter
|
}
|
||||||
};
|
};
|
||||||
|
APP.conference.createLocalTracks(type).then((tracks) => {
|
||||||
|
// FIXME does it mean that 'not track.length' == GUM failed ?
|
||||||
|
// And will this ever happen if promise is supposed to fail in GUM
|
||||||
|
// failed case ?
|
||||||
|
if (!tracks.length) {
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let stream = tracks[0];
|
||||||
|
|
||||||
|
// We now use screen stream
|
||||||
|
isUsingScreenStream = type === "desktop";
|
||||||
|
if (isUsingScreenStream) {
|
||||||
|
stream.on(TrackEvents.TRACK_STOPPED, onEndedHandler);
|
||||||
|
}
|
||||||
|
newStreamCreated(stream);
|
||||||
|
}).catch((error) => {
|
||||||
|
if(error === JitsiMeetJS.errors.track.FIREFOX_EXTENSION_NEEDED)
|
||||||
|
{
|
||||||
|
eventEmitter.emit(
|
||||||
|
DSEvents.FIREFOX_EXTENSION_NEEDED,
|
||||||
|
config.desktopSharingFirefoxExtensionURL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -21,20 +21,18 @@ function initShortcutHandlers() {
|
||||||
77: {
|
77: {
|
||||||
character: "M",
|
character: "M",
|
||||||
id: "mutePopover",
|
id: "mutePopover",
|
||||||
function: APP.UI.toggleAudio
|
function: APP.conference.toggleAudioMuted
|
||||||
},
|
},
|
||||||
84: {
|
84: {
|
||||||
character: "T",
|
character: "T",
|
||||||
function: function() {
|
function: function() {
|
||||||
if(!APP.RTC.localAudio.isMuted()) {
|
APP.conference.muteAudio(true);
|
||||||
APP.UI.toggleAudio();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
86: {
|
86: {
|
||||||
character: "V",
|
character: "V",
|
||||||
id: "toggleVideoPopover",
|
id: "toggleVideoPopover",
|
||||||
function: APP.UI.toggleVideo
|
function: APP.conference.toggleVideoMuted
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -67,9 +65,7 @@ var KeyboardShortcut = {
|
||||||
$(":focus").is("input[type=password]") ||
|
$(":focus").is("input[type=password]") ||
|
||||||
$(":focus").is("textarea"))) {
|
$(":focus").is("textarea"))) {
|
||||||
if(e.which === "T".charCodeAt(0)) {
|
if(e.which === "T".charCodeAt(0)) {
|
||||||
if(APP.RTC.localAudio.isMuted()) {
|
APP.conference.muteAudio(true);
|
||||||
APP.UI.toggleAudio();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
/* global APP, require, $ */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This module is meant to (eventually) contain and manage all information
|
|
||||||
* about members/participants of the conference, so that other modules don't
|
|
||||||
* have to do it on their own, and so that other modules can access members'
|
|
||||||
* information from a single place.
|
|
||||||
*
|
|
||||||
* Currently this module only manages information about the support of jingle
|
|
||||||
* DTMF of the members. Other fields, as well as accessor methods are meant to
|
|
||||||
* be added as needed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var Events = require("../../service/members/Events");
|
|
||||||
var EventEmitter = require("events");
|
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The actual container.
|
|
||||||
*/
|
|
||||||
var members = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There is at least one member that supports DTMF (i.e. is jigasi).
|
|
||||||
*/
|
|
||||||
var atLeastOneDtmf = false;
|
|
||||||
|
|
||||||
|
|
||||||
function registerListeners() {
|
|
||||||
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, onMucMemberJoined);
|
|
||||||
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a new member joining the MUC.
|
|
||||||
*/
|
|
||||||
function onMucMemberJoined(jid, id, displayName) {
|
|
||||||
var member = {
|
|
||||||
displayName: displayName
|
|
||||||
};
|
|
||||||
|
|
||||||
APP.xmpp.getConnection().disco.info(
|
|
||||||
jid, "" /* node */, function(iq) { onDiscoInfoReceived(jid, iq); });
|
|
||||||
|
|
||||||
members[jid] = member;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a member leaving the MUC.
|
|
||||||
*/
|
|
||||||
function onMucMemberLeft(jid) {
|
|
||||||
delete members[jid];
|
|
||||||
updateAtLeastOneDtmf();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the reception of a disco#info packet from a particular JID.
|
|
||||||
* @param jid the JID sending the packet.
|
|
||||||
* @param iq the packet.
|
|
||||||
*/
|
|
||||||
function onDiscoInfoReceived(jid, iq) {
|
|
||||||
if (!members[jid])
|
|
||||||
return;
|
|
||||||
|
|
||||||
var supportsDtmf
|
|
||||||
= $(iq).find('>query>feature[var="urn:xmpp:jingle:dtmf:0"]').length > 0;
|
|
||||||
updateDtmf(jid, supportsDtmf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the 'supportsDtmf' field for a member.
|
|
||||||
* @param jid the jid of the member.
|
|
||||||
* @param newValue the new value for the 'supportsDtmf' field.
|
|
||||||
*/
|
|
||||||
function updateDtmf(jid, newValue) {
|
|
||||||
var oldValue = members[jid].supportsDtmf;
|
|
||||||
members[jid].supportsDtmf = newValue;
|
|
||||||
|
|
||||||
if (newValue != oldValue) {
|
|
||||||
updateAtLeastOneDtmf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks each member's 'supportsDtmf' field and updates
|
|
||||||
* 'atLastOneSupportsDtmf'.
|
|
||||||
*/
|
|
||||||
function updateAtLeastOneDtmf() {
|
|
||||||
var newAtLeastOneDtmf = false;
|
|
||||||
for (var key in members) {
|
|
||||||
if (typeof members[key].supportsDtmf !== 'undefined'
|
|
||||||
&& members[key].supportsDtmf) {
|
|
||||||
newAtLeastOneDtmf= true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (atLeastOneDtmf != newAtLeastOneDtmf) {
|
|
||||||
atLeastOneDtmf = newAtLeastOneDtmf;
|
|
||||||
eventEmitter.emit(Events.DTMF_SUPPORT_CHANGED, atLeastOneDtmf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exported interface.
|
|
||||||
*/
|
|
||||||
var Members = {
|
|
||||||
start: function() {
|
|
||||||
registerListeners();
|
|
||||||
},
|
|
||||||
addListener: function(type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
},
|
|
||||||
removeListener: function (type, listener) {
|
|
||||||
eventEmitter.removeListener(type, listener);
|
|
||||||
},
|
|
||||||
size: function () {
|
|
||||||
return Object.keys(members).length;
|
|
||||||
},
|
|
||||||
getMembers: function () {
|
|
||||||
return members;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Members;
|
|
|
@ -1,11 +1,9 @@
|
||||||
var UsernameGenerator = require('../util/UsernameGenerator');
|
import {generateUsername} from '../util/UsernameGenerator';
|
||||||
|
|
||||||
var email = '';
|
var email = '';
|
||||||
var displayName = '';
|
var displayName = '';
|
||||||
var userId;
|
var userId;
|
||||||
var language = null;
|
var language = null;
|
||||||
var callStatsUserName;
|
|
||||||
|
|
||||||
|
|
||||||
function supportsLocalStorage() {
|
function supportsLocalStorage() {
|
||||||
try {
|
try {
|
||||||
|
@ -30,25 +28,16 @@ if (supportsLocalStorage()) {
|
||||||
console.log("generated id", window.localStorage.jitsiMeetId);
|
console.log("generated id", window.localStorage.jitsiMeetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.localStorage.callStatsUserName) {
|
|
||||||
window.localStorage.callStatsUserName
|
|
||||||
= UsernameGenerator.generateUsername();
|
|
||||||
console.log('generated callstats uid',
|
|
||||||
window.localStorage.callStatsUserName);
|
|
||||||
|
|
||||||
}
|
|
||||||
userId = window.localStorage.jitsiMeetId || '';
|
userId = window.localStorage.jitsiMeetId || '';
|
||||||
callStatsUserName = window.localStorage.callStatsUserName;
|
|
||||||
email = window.localStorage.email || '';
|
email = window.localStorage.email || '';
|
||||||
displayName = window.localStorage.displayname || '';
|
displayName = window.localStorage.displayname || '';
|
||||||
language = window.localStorage.language;
|
language = window.localStorage.language;
|
||||||
} else {
|
} else {
|
||||||
console.log("local storage is not supported");
|
console.log("local storage is not supported");
|
||||||
userId = generateUniqueId();
|
userId = generateUniqueId();
|
||||||
callStatsUserName = UsernameGenerator.generateUsername();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Settings = {
|
export default {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the local user display name and saves it to local storage
|
* Sets the local user display name and saves it to local storage
|
||||||
|
@ -57,6 +46,9 @@ var Settings = {
|
||||||
* @returns {string} the display name we just set
|
* @returns {string} the display name we just set
|
||||||
*/
|
*/
|
||||||
setDisplayName: function (newDisplayName) {
|
setDisplayName: function (newDisplayName) {
|
||||||
|
if (displayName === newDisplayName) {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
displayName = newDisplayName;
|
displayName = newDisplayName;
|
||||||
window.localStorage.displayname = displayName;
|
window.localStorage.displayname = displayName;
|
||||||
return displayName;
|
return displayName;
|
||||||
|
@ -70,20 +62,16 @@ var Settings = {
|
||||||
return displayName;
|
return displayName;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns fake username for callstats
|
|
||||||
* @returns {string} fake username for callstats
|
|
||||||
*/
|
|
||||||
getCallStatsUserName: function () {
|
|
||||||
return callStatsUserName;
|
|
||||||
},
|
|
||||||
|
|
||||||
setEmail: function (newEmail) {
|
setEmail: function (newEmail) {
|
||||||
email = newEmail;
|
email = newEmail;
|
||||||
window.localStorage.email = newEmail;
|
window.localStorage.email = newEmail;
|
||||||
return email;
|
return email;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getEmail: function () {
|
||||||
|
return email;
|
||||||
|
},
|
||||||
|
|
||||||
getSettings: function () {
|
getSettings: function () {
|
||||||
return {
|
return {
|
||||||
email: email,
|
email: email,
|
||||||
|
@ -92,10 +80,11 @@ var Settings = {
|
||||||
language: language
|
language: language
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getLanguage () {
|
||||||
|
return language;
|
||||||
|
},
|
||||||
setLanguage: function (lang) {
|
setLanguage: function (lang) {
|
||||||
language = lang;
|
language = lang;
|
||||||
window.localStorage.language = lang;
|
window.localStorage.language = lang;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Settings;
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
/* global config */
|
/* global config JitsiMeetJS */
|
||||||
|
|
||||||
var ScriptUtil = require('../util/ScriptUtil');
|
|
||||||
|
|
||||||
// Load the integration of a third-party analytics API such as Google Analytics.
|
// Load the integration of a third-party analytics API such as Google Analytics.
|
||||||
// Since we cannot guarantee the quality of the third-party service (e.g. their
|
// Since we cannot guarantee the quality of the third-party service (e.g. their
|
||||||
|
@ -11,27 +9,27 @@ var ScriptUtil = require('../util/ScriptUtil');
|
||||||
// its implementation asynchronously anyway so it makes sense to append the
|
// its implementation asynchronously anyway so it makes sense to append the
|
||||||
// loading on our side rather than prepend it.
|
// loading on our side rather than prepend it.
|
||||||
if (config.disableThirdPartyRequests !== true) {
|
if (config.disableThirdPartyRequests !== true) {
|
||||||
ScriptUtil.loadScript(
|
JitsiMeetJS.util.ScriptUtil.loadScript(
|
||||||
'analytics.js?v=1',
|
'analytics.js?v=1',
|
||||||
/* async */ true,
|
/* async */ true,
|
||||||
/* prepend */ false);
|
/* prepend */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoopAnalytics
|
class NoopAnalytics {
|
||||||
function NoopAnalytics() {}
|
sendEvent () {}
|
||||||
|
|
||||||
NoopAnalytics.prototype.sendEvent = function () {};
|
|
||||||
|
|
||||||
// AnalyticsAdapter
|
|
||||||
function AnalyticsAdapter() {
|
|
||||||
// XXX Since we asynchronously load the integration of the analytics API and
|
|
||||||
// the analytics API may asynchronously load its implementation (e.g. Google
|
|
||||||
// Analytics), we cannot make the decision with respect to which analytics
|
|
||||||
// implementation we will use here and we have to postpone it i.e. we will
|
|
||||||
// make a lazy decision.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsAdapter.prototype.sendEvent = function (action, data) {
|
// XXX Since we asynchronously load the integration of the analytics API and the
|
||||||
|
// analytics API may asynchronously load its implementation (e.g. Google
|
||||||
|
// Analytics), we cannot make the decision with respect to which analytics
|
||||||
|
// implementation we will use here and we have to postpone it i.e. we will make
|
||||||
|
// a lazy decision.
|
||||||
|
|
||||||
|
class AnalyticsAdapter {
|
||||||
|
constructor () {
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent (...args) {
|
||||||
var a = this.analytics;
|
var a = this.analytics;
|
||||||
|
|
||||||
if (a === null || typeof a === 'undefined') {
|
if (a === null || typeof a === 'undefined') {
|
||||||
|
@ -40,8 +38,9 @@ AnalyticsAdapter.prototype.sendEvent = function (action, data) {
|
||||||
this.analytics = a = new AnalyticsImpl();
|
this.analytics = a = new AnalyticsImpl();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
a.sendEvent.apply(a, arguments);
|
a.sendEvent(...args);
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = new AnalyticsAdapter();
|
export default new AnalyticsAdapter();
|
||||||
|
|
|
@ -1,273 +0,0 @@
|
||||||
/* global config, $, APP, Strophe, callstats */
|
|
||||||
|
|
||||||
var Settings = require('../settings/Settings');
|
|
||||||
var ScriptUtil = require('../util/ScriptUtil');
|
|
||||||
var jsSHA = require('jssha');
|
|
||||||
var io = require('socket.io-client');
|
|
||||||
var callStats = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @const
|
|
||||||
* @see http://www.callstats.io/api/#enumeration-of-wrtcfuncnames
|
|
||||||
*/
|
|
||||||
var wrtcFuncNames = {
|
|
||||||
createOffer: "createOffer",
|
|
||||||
createAnswer: "createAnswer",
|
|
||||||
setLocalDescription: "setLocalDescription",
|
|
||||||
setRemoteDescription: "setRemoteDescription",
|
|
||||||
addIceCandidate: "addIceCandidate",
|
|
||||||
getUserMedia: "getUserMedia"
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some errors may occur before CallStats.init in which case we will accumulate
|
|
||||||
* them and submit them to callstats.io on CallStats.init.
|
|
||||||
*/
|
|
||||||
var pendingErrors = [];
|
|
||||||
|
|
||||||
function initCallback (err, msg) {
|
|
||||||
console.log("CallStats Status: err=" + err + " msg=" + msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The indicator which determines whether the integration of callstats.io is
|
|
||||||
* enabled/allowed. Its value does not indicate whether the integration will
|
|
||||||
* succeed at runtime but rather whether it is to be attempted at runtime at
|
|
||||||
* all.
|
|
||||||
*/
|
|
||||||
var _enabled
|
|
||||||
= config.callStatsID && config.callStatsSecret
|
|
||||||
// Even though AppID and AppSecret may be specified, the integration of
|
|
||||||
// callstats.io may be disabled because of globally-disallowed requests
|
|
||||||
// to any third parties.
|
|
||||||
&& (config.disableThirdPartyRequests !== true);
|
|
||||||
|
|
||||||
if (_enabled) {
|
|
||||||
// Since callstats.io is a third party, we cannot guarantee the quality of
|
|
||||||
// their service. More specifically, their server may take noticeably long
|
|
||||||
// time to respond. Consequently, it is in our best interest (in the sense
|
|
||||||
// that the intergration of callstats.io is pretty important to us but not
|
|
||||||
// enough to allow it to prevent people from joining a conference) to (1)
|
|
||||||
// start downloading their API as soon as possible and (2) do the
|
|
||||||
// downloading asynchronously.
|
|
||||||
ScriptUtil.loadScript(
|
|
||||||
'https://api.callstats.io/static/callstats.min.js',
|
|
||||||
/* async */ true,
|
|
||||||
/* prepend */ true);
|
|
||||||
// FIXME At the time of this writing, we hope that the callstats.io API will
|
|
||||||
// have loaded by the time we needed it (i.e. CallStats.init is invoked).
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function which invokes f in a try/catch block, logs any exception
|
|
||||||
* to the console, and then swallows it.
|
|
||||||
*
|
|
||||||
* @param f the function to invoke in a try/catch block
|
|
||||||
* @return a function which invokes f in a try/catch block, logs any exception
|
|
||||||
* to the console, and then swallows it
|
|
||||||
*/
|
|
||||||
function _try_catch (f) {
|
|
||||||
return function () {
|
|
||||||
try {
|
|
||||||
f.apply(this, arguments);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var CallStats = {
|
|
||||||
init: _try_catch(function (jingleSession) {
|
|
||||||
if(!this.isEnabled() || callStats !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
callStats = new callstats($, io, jsSHA);
|
|
||||||
|
|
||||||
this.session = jingleSession;
|
|
||||||
this.peerconnection = jingleSession.peerconnection.peerconnection;
|
|
||||||
this.userID = Settings.getCallStatsUserName();
|
|
||||||
|
|
||||||
var location = window.location;
|
|
||||||
|
|
||||||
this.confID = location.hostname + location.pathname;
|
|
||||||
|
|
||||||
callStats.initialize(
|
|
||||||
config.callStatsID, config.callStatsSecret,
|
|
||||||
this.userID /* generated or given by the origin server */,
|
|
||||||
initCallback);
|
|
||||||
|
|
||||||
var usage = callStats.fabricUsage.multiplex;
|
|
||||||
|
|
||||||
callStats.addNewFabric(
|
|
||||||
this.peerconnection,
|
|
||||||
Strophe.getResourceFromJid(jingleSession.peerjid),
|
|
||||||
usage,
|
|
||||||
this.confID,
|
|
||||||
this.pcCallback.bind(this));
|
|
||||||
} catch (e) {
|
|
||||||
// The callstats.io API failed to initialize (e.g. because its
|
|
||||||
// download failed to succeed in general or on time). Further
|
|
||||||
// attempts to utilize it cannot possibly succeed.
|
|
||||||
callStats = null;
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
// Notify callstats about pre-init failures if there were any.
|
|
||||||
if (callStats && pendingErrors.length) {
|
|
||||||
pendingErrors.forEach(function (error) {
|
|
||||||
this._reportError(error.type, error.error, error.pc);
|
|
||||||
}, this);
|
|
||||||
pendingErrors.length = 0;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the callstats integration is enabled, otherwise returns
|
|
||||||
* false.
|
|
||||||
*
|
|
||||||
* @returns true if the callstats integration is enabled, otherwise returns
|
|
||||||
* false.
|
|
||||||
*/
|
|
||||||
isEnabled: function() {
|
|
||||||
return _enabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
pcCallback: _try_catch(function (err, msg) {
|
|
||||||
if (!callStats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("Monitoring status: "+ err + " msg: " + msg);
|
|
||||||
callStats.sendFabricEvent(this.peerconnection,
|
|
||||||
callStats.fabricEvent.fabricSetup, this.confID);
|
|
||||||
}),
|
|
||||||
|
|
||||||
sendMuteEvent: _try_catch(function (mute, type) {
|
|
||||||
if (!callStats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var event = null;
|
|
||||||
if (type === "video") {
|
|
||||||
event = (mute? callStats.fabricEvent.videoPause :
|
|
||||||
callStats.fabricEvent.videoResume);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
event = (mute? callStats.fabricEvent.audioMute :
|
|
||||||
callStats.fabricEvent.audioUnmute);
|
|
||||||
}
|
|
||||||
callStats.sendFabricEvent(this.peerconnection, event, this.confID);
|
|
||||||
}),
|
|
||||||
|
|
||||||
sendTerminateEvent: _try_catch(function () {
|
|
||||||
if(!callStats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callStats.sendFabricEvent(this.peerconnection,
|
|
||||||
callStats.fabricEvent.fabricTerminated, this.confID);
|
|
||||||
}),
|
|
||||||
|
|
||||||
sendSetupFailedEvent: _try_catch(function () {
|
|
||||||
if(!callStats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callStats.sendFabricEvent(this.peerconnection,
|
|
||||||
callStats.fabricEvent.fabricSetupFailed, this.confID);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the given feedback through CallStats.
|
|
||||||
*
|
|
||||||
* @param overallFeedback an integer between 1 and 5 indicating the
|
|
||||||
* user feedback
|
|
||||||
* @param detailedFeedback detailed feedback from the user. Not yet used
|
|
||||||
*/
|
|
||||||
sendFeedback: _try_catch(function(overallFeedback, detailedFeedback) {
|
|
||||||
if(!callStats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var feedbackString = '{"userID":"' + this.userID + '"' +
|
|
||||||
', "overall":' + overallFeedback +
|
|
||||||
', "comment": "' + detailedFeedback + '"}';
|
|
||||||
|
|
||||||
var feedbackJSON = JSON.parse(feedbackString);
|
|
||||||
|
|
||||||
callStats.sendUserFeedback(this.confID, feedbackJSON);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reports an error to callstats.
|
|
||||||
*
|
|
||||||
* @param type the type of the error, which will be one of the wrtcFuncNames
|
|
||||||
* @param e the error
|
|
||||||
* @param pc the peerconnection
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_reportError: function (type, e, pc) {
|
|
||||||
if (callStats) {
|
|
||||||
callStats.reportError(pc, this.confID, type, e);
|
|
||||||
} else if (this.isEnabled()) {
|
|
||||||
pendingErrors.push({ type: type, error: e, pc: pc });
|
|
||||||
}
|
|
||||||
// else just ignore it
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that getUserMedia failed.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
*/
|
|
||||||
sendGetUserMediaFailed: _try_catch(function (e) {
|
|
||||||
this._reportError(wrtcFuncNames.getUserMedia, e, null);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that peer connection failed to create offer.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
* @param {RTCPeerConnection} pc connection on which failure occured.
|
|
||||||
*/
|
|
||||||
sendCreateOfferFailed: _try_catch(function (e, pc) {
|
|
||||||
this._reportError(wrtcFuncNames.createOffer, e, pc);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that peer connection failed to create answer.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
* @param {RTCPeerConnection} pc connection on which failure occured.
|
|
||||||
*/
|
|
||||||
sendCreateAnswerFailed: _try_catch(function (e, pc) {
|
|
||||||
this._reportError(wrtcFuncNames.createAnswer, e, pc);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that peer connection failed to set local description.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
* @param {RTCPeerConnection} pc connection on which failure occured.
|
|
||||||
*/
|
|
||||||
sendSetLocalDescFailed: _try_catch(function (e, pc) {
|
|
||||||
this._reportError(wrtcFuncNames.setLocalDescription, e, pc);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that peer connection failed to set remote description.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
* @param {RTCPeerConnection} pc connection on which failure occured.
|
|
||||||
*/
|
|
||||||
sendSetRemoteDescFailed: _try_catch(function (e, pc) {
|
|
||||||
this._reportError(wrtcFuncNames.setRemoteDescription, e, pc);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies CallStats that peer connection failed to add ICE candidate.
|
|
||||||
*
|
|
||||||
* @param {Error} e error to send
|
|
||||||
* @param {RTCPeerConnection} pc connection on which failure occured.
|
|
||||||
*/
|
|
||||||
sendAddIceCandidateFailed: _try_catch(function (e, pc) {
|
|
||||||
this._reportError(wrtcFuncNames.addIceCandidate, e, pc);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
module.exports = CallStats;
|
|
|
@ -1,128 +0,0 @@
|
||||||
/* global config, AudioContext */
|
|
||||||
/**
|
|
||||||
* Provides statistics for the local stream.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
|
||||||
var StatisticsEvents = require('../../service/statistics/Events');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Size of the webaudio analyzer buffer.
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
var WEBAUDIO_ANALYZER_FFT_SIZE = 2048;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value of the webaudio analyzer smoothing time parameter.
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
var WEBAUDIO_ANALYZER_SMOOTING_TIME = 0.8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts time domain data array to audio level.
|
|
||||||
* @param samples the time domain data array.
|
|
||||||
* @returns {number} the audio level
|
|
||||||
*/
|
|
||||||
function timeDomainDataToAudioLevel(samples) {
|
|
||||||
|
|
||||||
var maxVolume = 0;
|
|
||||||
|
|
||||||
var length = samples.length;
|
|
||||||
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
if (maxVolume < samples[i])
|
|
||||||
maxVolume = samples[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(((maxVolume - 127) / 128).toFixed(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animates audio level change
|
|
||||||
* @param newLevel the new audio level
|
|
||||||
* @param lastLevel the last audio level
|
|
||||||
* @returns {Number} the audio level to be set
|
|
||||||
*/
|
|
||||||
function animateLevel(newLevel, lastLevel) {
|
|
||||||
var value = 0;
|
|
||||||
var diff = lastLevel - newLevel;
|
|
||||||
if(diff > 0.2) {
|
|
||||||
value = lastLevel - 0.2;
|
|
||||||
}
|
|
||||||
else if(diff < -0.4) {
|
|
||||||
value = lastLevel + 0.4;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value = newLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(value.toFixed(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
|
|
||||||
*
|
|
||||||
* @param stream the local stream
|
|
||||||
* @param interval stats refresh interval given in ms.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function LocalStatsCollector(stream, interval,
|
|
||||||
statisticsService, eventEmitter) {
|
|
||||||
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
||||||
this.stream = stream;
|
|
||||||
this.intervalId = null;
|
|
||||||
this.intervalMilis = interval;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
this.audioLevel = 0;
|
|
||||||
this.statisticsService = statisticsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the collecting the statistics.
|
|
||||||
*/
|
|
||||||
LocalStatsCollector.prototype.start = function () {
|
|
||||||
if (config.disableAudioLevels || !window.AudioContext ||
|
|
||||||
RTCBrowserType.isTemasysPluginUsed())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var context = new AudioContext();
|
|
||||||
var analyser = context.createAnalyser();
|
|
||||||
analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
|
|
||||||
analyser.fftSize = WEBAUDIO_ANALYZER_FFT_SIZE;
|
|
||||||
|
|
||||||
|
|
||||||
var source = context.createMediaStreamSource(this.stream);
|
|
||||||
source.connect(analyser);
|
|
||||||
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.intervalId = setInterval(
|
|
||||||
function () {
|
|
||||||
var array = new Uint8Array(analyser.frequencyBinCount);
|
|
||||||
analyser.getByteTimeDomainData(array);
|
|
||||||
var audioLevel = timeDomainDataToAudioLevel(array);
|
|
||||||
if (audioLevel != self.audioLevel) {
|
|
||||||
self.audioLevel = animateLevel(audioLevel, self.audioLevel);
|
|
||||||
self.eventEmitter.emit(
|
|
||||||
StatisticsEvents.AUDIO_LEVEL,
|
|
||||||
self.statisticsService.LOCAL_JID,
|
|
||||||
self.audioLevel);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
this.intervalMilis
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops collecting the statistics.
|
|
||||||
*/
|
|
||||||
LocalStatsCollector.prototype.stop = function () {
|
|
||||||
if (this.intervalId) {
|
|
||||||
clearInterval(this.intervalId);
|
|
||||||
this.intervalId = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LocalStatsCollector;
|
|
|
@ -146,7 +146,6 @@ function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, even
|
||||||
{
|
{
|
||||||
this.peerconnection = peerconnection;
|
this.peerconnection = peerconnection;
|
||||||
this.baselineAudioLevelsReport = null;
|
this.baselineAudioLevelsReport = null;
|
||||||
this.currentAudioLevelsReport = null;
|
|
||||||
this.currentStatsReport = null;
|
this.currentStatsReport = null;
|
||||||
this.baselineStatsReport = null;
|
this.baselineStatsReport = null;
|
||||||
this.audioLevelsIntervalId = null;
|
this.audioLevelsIntervalId = null;
|
||||||
|
@ -252,10 +251,7 @@ StatsCollector.prototype.start = function ()
|
||||||
results = report.result();
|
results = report.result();
|
||||||
}
|
}
|
||||||
//console.error("Got interval report", results);
|
//console.error("Got interval report", results);
|
||||||
self.currentAudioLevelsReport = results;
|
self.baselineAudioLevelsReport = results;
|
||||||
self.processAudioLevelReport();
|
|
||||||
self.baselineAudioLevelsReport =
|
|
||||||
self.currentAudioLevelsReport;
|
|
||||||
},
|
},
|
||||||
self.errorCallback
|
self.errorCallback
|
||||||
);
|
);
|
||||||
|
@ -385,7 +381,7 @@ StatsCollector.prototype.addStatsToBeLogged = function (reports) {
|
||||||
|
|
||||||
StatsCollector.prototype.logStats = function () {
|
StatsCollector.prototype.logStats = function () {
|
||||||
|
|
||||||
if(!APP.xmpp.sendLogs(this.statsToBeLogged))
|
if(!APP.conference._room.xmpp.sendLogs(this.statsToBeLogged))
|
||||||
return;
|
return;
|
||||||
// Reset the stats
|
// Reset the stats
|
||||||
this.statsToBeLogged.stats = {};
|
this.statsToBeLogged.stats = {};
|
||||||
|
@ -501,7 +497,7 @@ StatsCollector.prototype.processStatsReport = function () {
|
||||||
var ssrc = getStatValue(now, 'ssrc');
|
var ssrc = getStatValue(now, 'ssrc');
|
||||||
if(!ssrc)
|
if(!ssrc)
|
||||||
continue;
|
continue;
|
||||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
var jid = APP.conference._room.room.getJidBySSRC(ssrc);
|
||||||
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
||||||
console.warn("No jid for ssrc: " + ssrc);
|
console.warn("No jid for ssrc: " + ssrc);
|
||||||
continue;
|
continue;
|
||||||
|
@ -647,76 +643,22 @@ StatsCollector.prototype.processStatsReport = function () {
|
||||||
upload:
|
upload:
|
||||||
calculatePacketLoss(lostPackets.upload, totalPackets.upload)
|
calculatePacketLoss(lostPackets.upload, totalPackets.upload)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let idResolution = {};
|
||||||
|
if (resolutions) { // use id instead of jid
|
||||||
|
Object.keys(resolutions).forEach(function (jid) {
|
||||||
|
let id = Strophe.getResourceFromJid(jid);
|
||||||
|
idResolution[id] = resolutions[jid];
|
||||||
|
});
|
||||||
|
}
|
||||||
this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS,
|
this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS,
|
||||||
{
|
{
|
||||||
"bitrate": PeerStats.bitrate,
|
"bitrate": PeerStats.bitrate,
|
||||||
"packetLoss": PeerStats.packetLoss,
|
"packetLoss": PeerStats.packetLoss,
|
||||||
"bandwidth": PeerStats.bandwidth,
|
"bandwidth": PeerStats.bandwidth,
|
||||||
"resolution": resolutions,
|
"resolution": idResolution,
|
||||||
"transport": PeerStats.transport
|
"transport": PeerStats.transport
|
||||||
});
|
});
|
||||||
PeerStats.transport = [];
|
PeerStats.transport = [];
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Stats processing logic.
|
|
||||||
*/
|
|
||||||
StatsCollector.prototype.processAudioLevelReport = function () {
|
|
||||||
if (!this.baselineAudioLevelsReport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var idx in this.currentAudioLevelsReport) {
|
|
||||||
var now = this.currentAudioLevelsReport[idx];
|
|
||||||
|
|
||||||
if (now.type != 'ssrc') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var before = this.baselineAudioLevelsReport[idx];
|
|
||||||
if (!before) {
|
|
||||||
console.warn(getStatValue(now, 'ssrc') + ' not enough data');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ssrc = getStatValue(now, 'ssrc');
|
|
||||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
|
||||||
if (!jid) {
|
|
||||||
if((Date.now() - now.timestamp) < 3000)
|
|
||||||
console.warn("No jid for ssrc: " + ssrc);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var jidStats = this.jid2stats[jid];
|
|
||||||
if (!jidStats) {
|
|
||||||
jidStats = new PeerStats();
|
|
||||||
this.jid2stats[jid] = jidStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio level
|
|
||||||
var audioLevel = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
audioLevel = getStatValue(now, 'audioInputLevel');
|
|
||||||
if (!audioLevel)
|
|
||||||
audioLevel = getStatValue(now, 'audioOutputLevel');
|
|
||||||
}
|
|
||||||
catch(e) {/*not supported*/
|
|
||||||
console.warn("Audio Levels are not available in the statistics.");
|
|
||||||
clearInterval(this.audioLevelsIntervalId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioLevel) {
|
|
||||||
// TODO: can't find specs about what this value really is,
|
|
||||||
// but it seems to vary between 0 and around 32k.
|
|
||||||
audioLevel = audioLevel / 32767;
|
|
||||||
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
|
|
||||||
if (jid != APP.xmpp.myJid()) {
|
|
||||||
this.eventEmitter.emit(
|
|
||||||
StatisticsEvents.AUDIO_LEVEL, jid, audioLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -2,28 +2,17 @@
|
||||||
/**
|
/**
|
||||||
* Created by hristo on 8/4/14.
|
* Created by hristo on 8/4/14.
|
||||||
*/
|
*/
|
||||||
var LocalStats = require("./LocalStatsCollector.js");
|
|
||||||
var RTPStats = require("./RTPStatsCollector.js");
|
var RTPStats = require("./RTPStatsCollector.js");
|
||||||
var EventEmitter = require("events");
|
var EventEmitter = require("events");
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||||
var CallStats = require("./CallStats");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||||
var StatisticsEvents = require("../../service/statistics/Events");
|
var StatisticsEvents = require("../../service/statistics/Events");
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
var eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
var localStats = null;
|
|
||||||
|
|
||||||
var rtpStats = null;
|
var rtpStats = null;
|
||||||
|
|
||||||
function stopLocal() {
|
|
||||||
if (localStats) {
|
|
||||||
localStats.stop();
|
|
||||||
localStats = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopRemote() {
|
function stopRemote() {
|
||||||
if (rtpStats) {
|
if (rtpStats) {
|
||||||
rtpStats.stop();
|
rtpStats.stop();
|
||||||
|
@ -41,26 +30,14 @@ function startRemoteStats (peerconnection) {
|
||||||
rtpStats.start();
|
rtpStats.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamCreated(stream) {
|
|
||||||
if(stream.getOriginalStream().getAudioTracks().length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStats = new LocalStats(stream.getOriginalStream(), 200, statistics,
|
|
||||||
eventEmitter);
|
|
||||||
localStats.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisposeConference(onUnload) {
|
function onDisposeConference(onUnload) {
|
||||||
CallStats.sendTerminateEvent();
|
|
||||||
stopRemote();
|
stopRemote();
|
||||||
if(onUnload) {
|
if (onUnload) {
|
||||||
stopLocal();
|
|
||||||
eventEmitter.removeAllListeners();
|
eventEmitter.removeAllListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var statistics = {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Indicates that this audio level is for local jid.
|
* Indicates that this audio level is for local jid.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -74,89 +51,21 @@ var statistics = {
|
||||||
eventEmitter.removeListener(type, listener);
|
eventEmitter.removeListener(type, listener);
|
||||||
},
|
},
|
||||||
stop: function () {
|
stop: function () {
|
||||||
stopLocal();
|
|
||||||
stopRemote();
|
stopRemote();
|
||||||
if(eventEmitter)
|
if (eventEmitter) {
|
||||||
{
|
|
||||||
eventEmitter.removeAllListeners();
|
eventEmitter.removeAllListeners();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopRemoteStatistics: function()
|
|
||||||
{
|
|
||||||
stopRemote();
|
|
||||||
},
|
|
||||||
start: function () {
|
start: function () {
|
||||||
APP.RTC.addStreamListener(onStreamCreated,
|
const xmpp = APP.conference._room.xmpp;
|
||||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
xmpp.addListener(
|
||||||
APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE,
|
XMPPEvents.DISPOSE_CONFERENCE,
|
||||||
onDisposeConference);
|
onDisposeConference
|
||||||
|
);
|
||||||
//FIXME: we may want to change CALL INCOMING event to
|
//FIXME: we may want to change CALL INCOMING event to
|
||||||
// onnegotiationneeded
|
// onnegotiationneeded
|
||||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
||||||
startRemoteStats(event.peerconnection);
|
startRemoteStats(event.peerconnection);
|
||||||
// CallStats.init(event);
|
|
||||||
});
|
});
|
||||||
APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY,
|
|
||||||
function (session) {
|
|
||||||
CallStats.init(session);
|
|
||||||
});
|
|
||||||
APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) {
|
|
||||||
CallStats.sendMuteEvent(mute, "audio");
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
|
|
||||||
CallStats.sendSetupFailedEvent();
|
|
||||||
});
|
|
||||||
APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) {
|
|
||||||
CallStats.sendMuteEvent(mute, "video");
|
|
||||||
});
|
|
||||||
|
|
||||||
APP.RTC.addListener(RTCEvents.GET_USER_MEDIA_FAILED, function (e) {
|
|
||||||
CallStats.sendGetUserMediaFailed(e);
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e, pc) {
|
|
||||||
CallStats.sendCreateOfferFailed(e, pc);
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e, pc) {
|
|
||||||
CallStats.sendCreateAnswerFailed(e, pc);
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(
|
|
||||||
RTCEvents.SET_LOCAL_DESCRIPTION_FAILED,
|
|
||||||
function (e, pc) {
|
|
||||||
CallStats.sendSetLocalDescFailed(e, pc);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
APP.xmpp.addListener(
|
|
||||||
RTCEvents.SET_REMOTE_DESCRIPTION_FAILED,
|
|
||||||
function (e, pc) {
|
|
||||||
CallStats.sendSetRemoteDescFailed(e, pc);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
APP.xmpp.addListener(
|
|
||||||
RTCEvents.ADD_ICE_CANDIDATE_FAILED,
|
|
||||||
function (e, pc) {
|
|
||||||
CallStats.sendAddIceCandidateFailed(e, pc);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Obtains audio level reported in the stats for specified peer.
|
|
||||||
* @param peerJid full MUC jid of the user for whom we want to obtain last
|
|
||||||
* audio level.
|
|
||||||
* @param ssrc the SSRC of audio stream for which we want to obtain audio
|
|
||||||
* level.
|
|
||||||
* @returns {*} a float form 0 to 1 that represents current audio level or
|
|
||||||
* <tt>null</tt> if for any reason the value is not available
|
|
||||||
* at this time.
|
|
||||||
*/
|
|
||||||
getPeerSSRCAudioLevel: function (peerJid, ssrc) {
|
|
||||||
|
|
||||||
var peerStats = rtpStats.jid2stats[peerJid];
|
|
||||||
|
|
||||||
return peerStats ? peerStats.ssrc2AudioLevel[ssrc] : null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = statistics;
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* global $, require, config, interfaceConfig */
|
/* global $, require, config, interfaceConfig */
|
||||||
var i18n = require("i18next-client");
|
var i18n = require("i18next-client");
|
||||||
var languages = require("../../service/translation/languages");
|
var languages = require("../../service/translation/languages");
|
||||||
var Settings = require("../settings/Settings");
|
|
||||||
var DEFAULT_LANG = languages.EN;
|
var DEFAULT_LANG = languages.EN;
|
||||||
|
|
||||||
i18n.addPostProcessor("resolveAppName", function(value, key, options) {
|
i18n.addPostProcessor("resolveAppName", function(value, key, options) {
|
||||||
|
@ -68,7 +67,7 @@ function initCompleted(t) {
|
||||||
$("[data-i18n]").i18n();
|
$("[data-i18n]").i18n();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForParameter() {
|
function getLangFromQuery() {
|
||||||
var query = window.location.search.substring(1);
|
var query = window.location.search.substring(1);
|
||||||
var vars = query.split("&");
|
var vars = query.split("&");
|
||||||
for (var i=0;i<vars.length;i++) {
|
for (var i=0;i<vars.length;i++) {
|
||||||
|
@ -82,27 +81,11 @@ function checkForParameter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init: function (lang) {
|
init: function (settingsLang) {
|
||||||
var options = defaultOptions;
|
let options = defaultOptions;
|
||||||
|
|
||||||
|
let lang = getLangFromQuery() || settingsLang || config.defaultLanguage;
|
||||||
if(!lang)
|
if (lang) {
|
||||||
{
|
|
||||||
lang = checkForParameter();
|
|
||||||
if(!lang)
|
|
||||||
{
|
|
||||||
var settings = Settings.getSettings();
|
|
||||||
if(settings)
|
|
||||||
lang = settings.language;
|
|
||||||
|
|
||||||
if(!lang && config.defaultLanguage)
|
|
||||||
{
|
|
||||||
lang = config.defaultLanguage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(lang) {
|
|
||||||
options.lng = lang;
|
options.lng = lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,8 +107,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
generateTranslationHTML: function (key, options) {
|
generateTranslationHTML: function (key, options) {
|
||||||
var str = "<span data-i18n=\"" + key + "\"";
|
var str = "<span data-i18n=\"" + key + "\"";
|
||||||
if(options)
|
if (options) {
|
||||||
{
|
|
||||||
str += " data-i18n-options=\"" + JSON.stringify(options) + "\"";
|
str += " data-i18n-options=\"" + JSON.stringify(options) + "\"";
|
||||||
}
|
}
|
||||||
str += ">";
|
str += ">";
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
/**
|
|
||||||
* Implements utility functions which facilitate the dealing with scripts such
|
|
||||||
* as the download and execution of a JavaScript file.
|
|
||||||
*/
|
|
||||||
var ScriptUtil = {
|
|
||||||
/**
|
|
||||||
* Loads a script from a specific source.
|
|
||||||
*
|
|
||||||
* @param src the source from the which the script is to be (down)loaded
|
|
||||||
* @param async true to asynchronously load the script or false to
|
|
||||||
* synchronously load the script
|
|
||||||
* @param prepend true to schedule the loading of the script as soon as
|
|
||||||
* possible or false to schedule the loading of the script at the end of the
|
|
||||||
* scripts known at the time
|
|
||||||
*/
|
|
||||||
loadScript: function (src, async, prepend) {
|
|
||||||
var d = document;
|
|
||||||
var tagName = 'script';
|
|
||||||
var script = d.createElement(tagName);
|
|
||||||
var referenceNode = d.getElementsByTagName(tagName)[0];
|
|
||||||
|
|
||||||
script.async = async;
|
|
||||||
script.src = src;
|
|
||||||
if (prepend) {
|
|
||||||
referenceNode.parentNode.insertBefore(script, referenceNode);
|
|
||||||
} else {
|
|
||||||
referenceNode.parentNode.appendChild(script);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = ScriptUtil;
|
|
|
@ -1,4 +1,4 @@
|
||||||
var RandomUtil = require('./RandomUtil');
|
import RandomUtil from './RandomUtil';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* from faker.js - Copyright (c) 2014-2015 Matthew Bergman & Marak Squires
|
* from faker.js - Copyright (c) 2014-2015 Matthew Bergman & Marak Squires
|
||||||
|
@ -417,13 +417,9 @@ var names = [
|
||||||
* Generate random username.
|
* Generate random username.
|
||||||
* @returns {string} random username
|
* @returns {string} random username
|
||||||
*/
|
*/
|
||||||
function generateUsername () {
|
export function generateUsername () {
|
||||||
var name = RandomUtil.randomElement(names);
|
var name = RandomUtil.randomElement(names);
|
||||||
var suffix = RandomUtil.randomAlphanumStr(3);
|
var suffix = RandomUtil.randomAlphanumStr(3);
|
||||||
|
|
||||||
return name + '-' + suffix;
|
return name + '-' + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generateUsername: generateUsername
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Create deferred object.
|
||||||
|
* @returns {{promise, resolve, reject}}
|
||||||
|
*/
|
||||||
|
export function createDeferred () {
|
||||||
|
let deferred = {};
|
||||||
|
|
||||||
|
deferred.promise = new Promise(function (resolve, reject) {
|
||||||
|
deferred.resolve = resolve;
|
||||||
|
deferred.reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred;
|
||||||
|
}
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
* JingleSession provides an API to manage a single Jingle session. We will
|
|
||||||
* have different implementations depending on the underlying interface used
|
|
||||||
* (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
|
|
||||||
*/
|
|
||||||
function JingleSession(me, sid, connection, service, eventEmitter) {
|
|
||||||
/**
|
|
||||||
* Our JID.
|
|
||||||
*/
|
|
||||||
this.me = me;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Jingle session identifier.
|
|
||||||
*/
|
|
||||||
this.sid = sid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The XMPP connection.
|
|
||||||
*/
|
|
||||||
this.connection = connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The XMPP service.
|
|
||||||
*/
|
|
||||||
this.service = service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The event emitter.
|
|
||||||
*/
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to use dripping or not. Dripping is sending trickle candidates
|
|
||||||
* not one-by-one.
|
|
||||||
* Note: currently we do not support 'false'.
|
|
||||||
*/
|
|
||||||
this.usedrip = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When dripping is used, stores ICE candidates which are to be sent.
|
|
||||||
*/
|
|
||||||
this.drip_container = [];
|
|
||||||
|
|
||||||
// Media constraints. Is this WebRTC only?
|
|
||||||
this.media_constraints = null;
|
|
||||||
|
|
||||||
// ICE servers config (RTCConfiguration?).
|
|
||||||
this.ice_config = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares this object to initiate a session.
|
|
||||||
* @param peerjid the JID of the remote peer.
|
|
||||||
* @param isInitiator whether we will be the Jingle initiator.
|
|
||||||
* @param media_constraints
|
|
||||||
* @param ice_config
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.initialize = function(peerjid, isInitiator,
|
|
||||||
media_constraints, ice_config) {
|
|
||||||
this.media_constraints = media_constraints;
|
|
||||||
this.ice_config = ice_config;
|
|
||||||
|
|
||||||
if (this.state !== null) {
|
|
||||||
console.error('attempt to initiate on session ' + this.sid +
|
|
||||||
'in state ' + this.state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.state = 'pending';
|
|
||||||
this.initiator = isInitiator ? this.me : peerjid;
|
|
||||||
this.responder = !isInitiator ? this.me : peerjid;
|
|
||||||
this.peerjid = peerjid;
|
|
||||||
|
|
||||||
this.doInitialize();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finishes initialization.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.doInitialize = function() {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the ICE candidates found in the 'contents' array as remote candidates?
|
|
||||||
* Note: currently only used on transport-info
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.addIceCandidates = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an 'add-source' event.
|
|
||||||
*
|
|
||||||
* @param contents an array of Jingle 'content' elements.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.addSources = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a 'remove-source' event.
|
|
||||||
*
|
|
||||||
* @param contents an array of Jingle 'content' elements.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.removeSources = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Terminates this Jingle session (stops sending media and closes the streams?)
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.terminate = function() {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a Jingle session-terminate message to the peer and terminates the
|
|
||||||
* session.
|
|
||||||
* @param reason
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.sendTerminate = function(reason, text) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an offer from the remote peer (prepares to accept a session).
|
|
||||||
* @param jingle the 'jingle' XML element.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.setOffer = function(jingle) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an answer from the remote peer (prepares to accept a session).
|
|
||||||
* @param jingle the 'jingle' XML element.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.setAnswer = function(jingle) {};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = JingleSession;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,643 +0,0 @@
|
||||||
/* jshint -W101 */
|
|
||||||
/* jshint -W117 */
|
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
|
|
||||||
// SDP STUFF
|
|
||||||
function SDP(sdp) {
|
|
||||||
/**
|
|
||||||
* Whether or not to remove TCP ice candidates when translating from/to jingle.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.removeTcpCandidates = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not to remove UDP ice candidates when translating from/to jingle.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.removeUdpCandidates = false;
|
|
||||||
|
|
||||||
this.media = sdp.split('\r\nm=');
|
|
||||||
for (var i = 1; i < this.media.length; i++) {
|
|
||||||
this.media[i] = 'm=' + this.media[i];
|
|
||||||
if (i != this.media.length - 1) {
|
|
||||||
this.media[i] += '\r\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.session = this.media.shift() + '\r\n';
|
|
||||||
this.raw = this.session + this.media.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns map of MediaChannel mapped per channel idx.
|
|
||||||
*/
|
|
||||||
SDP.prototype.getMediaSsrcMap = function() {
|
|
||||||
var self = this;
|
|
||||||
var media_ssrcs = {};
|
|
||||||
var tmp;
|
|
||||||
for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
|
|
||||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
|
|
||||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
|
|
||||||
var media = {
|
|
||||||
mediaindex: mediaindex,
|
|
||||||
mid: mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
media_ssrcs[mediaindex] = media;
|
|
||||||
tmp.forEach(function (line) {
|
|
||||||
var linessrc = line.substring(7).split(' ')[0];
|
|
||||||
// allocate new ChannelSsrc
|
|
||||||
if(!media.ssrcs[linessrc]) {
|
|
||||||
media.ssrcs[linessrc] = {
|
|
||||||
ssrc: linessrc,
|
|
||||||
lines: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
media.ssrcs[linessrc].lines.push(line);
|
|
||||||
});
|
|
||||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
|
|
||||||
tmp.forEach(function(line){
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var semantics = line.substr(0, idx).substr(13);
|
|
||||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
|
||||||
if (ssrcs.length) {
|
|
||||||
media.ssrcGroups.push({
|
|
||||||
semantics: semantics,
|
|
||||||
ssrcs: ssrcs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return media_ssrcs;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Returns <tt>true</tt> if this SDP contains given SSRC.
|
|
||||||
* @param ssrc the ssrc to check.
|
|
||||||
* @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
|
|
||||||
*/
|
|
||||||
SDP.prototype.containsSSRC = function (ssrc) {
|
|
||||||
// FIXME this code is really strange - improve it if you can
|
|
||||||
var medias = this.getMediaSsrcMap();
|
|
||||||
var result = false;
|
|
||||||
Object.keys(medias).forEach(function (mediaindex) {
|
|
||||||
if (result)
|
|
||||||
return;
|
|
||||||
if (medias[mediaindex].ssrcs[ssrc]) {
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove iSAC and CN from SDP
|
|
||||||
SDP.prototype.mangle = function () {
|
|
||||||
var i, j, mline, lines, rtpmap, newdesc;
|
|
||||||
for (i = 0; i < this.media.length; i++) {
|
|
||||||
lines = this.media[i].split('\r\n');
|
|
||||||
lines.pop(); // remove empty last element
|
|
||||||
mline = SDPUtil.parse_mline(lines.shift());
|
|
||||||
if (mline.media != 'audio')
|
|
||||||
continue;
|
|
||||||
newdesc = '';
|
|
||||||
mline.fmt.length = 0;
|
|
||||||
for (j = 0; j < lines.length; j++) {
|
|
||||||
if (lines[j].substr(0, 9) == 'a=rtpmap:') {
|
|
||||||
rtpmap = SDPUtil.parse_rtpmap(lines[j]);
|
|
||||||
if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
|
|
||||||
continue;
|
|
||||||
mline.fmt.push(rtpmap.id);
|
|
||||||
newdesc += lines[j] + '\r\n';
|
|
||||||
} else {
|
|
||||||
newdesc += lines[j] + '\r\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
|
|
||||||
this.media[i] += newdesc;
|
|
||||||
}
|
|
||||||
this.raw = this.session + this.media.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove lines matching prefix from session section
|
|
||||||
SDP.prototype.removeSessionLines = function(prefix) {
|
|
||||||
var self = this;
|
|
||||||
var lines = SDPUtil.find_lines(this.session, prefix);
|
|
||||||
lines.forEach(function(line) {
|
|
||||||
self.session = self.session.replace(line + '\r\n', '');
|
|
||||||
});
|
|
||||||
this.raw = this.session + this.media.join('');
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove lines matching prefix from a media section specified by mediaindex
|
|
||||||
// TODO: non-numeric mediaindex could match mid
|
|
||||||
SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
|
|
||||||
var self = this;
|
|
||||||
var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
|
|
||||||
lines.forEach(function(line) {
|
|
||||||
self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
|
|
||||||
});
|
|
||||||
this.raw = this.session + this.media.join('');
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
// add content's to a jingle element
|
|
||||||
SDP.prototype.toJingle = function (elem, thecreator) {
|
|
||||||
// console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
|
|
||||||
var i, j, k, mline, ssrc, rtpmap, tmp, lines;
|
|
||||||
// new bundle plan
|
|
||||||
if (SDPUtil.find_line(this.session, 'a=group:')) {
|
|
||||||
lines = SDPUtil.find_lines(this.session, 'a=group:');
|
|
||||||
for (i = 0; i < lines.length; i++) {
|
|
||||||
tmp = lines[i].split(' ');
|
|
||||||
var semantics = tmp.shift().substr(8);
|
|
||||||
elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
|
|
||||||
for (j = 0; j < tmp.length; j++) {
|
|
||||||
elem.c('content', {name: tmp[j]}).up();
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i = 0; i < this.media.length; i++) {
|
|
||||||
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
|
|
||||||
if (!(mline.media === 'audio' ||
|
|
||||||
mline.media === 'video' ||
|
|
||||||
mline.media === 'application'))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
|
||||||
ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
|
|
||||||
} else {
|
|
||||||
ssrc = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.c('content', {creator: thecreator, name: mline.media});
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
|
|
||||||
// prefer identifier from a=mid if present
|
|
||||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
|
|
||||||
elem.attrs({ name: mid });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
|
|
||||||
elem.c('description',
|
|
||||||
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
|
||||||
media: mline.media });
|
|
||||||
if (ssrc) {
|
|
||||||
elem.attrs({ssrc: ssrc});
|
|
||||||
}
|
|
||||||
for (j = 0; j < mline.fmt.length; j++) {
|
|
||||||
rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
|
|
||||||
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
|
||||||
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
|
|
||||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
|
|
||||||
for (k = 0; k < tmp.length; k++) {
|
|
||||||
elem.c('parameter', tmp[k]).up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.rtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
|
|
||||||
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
|
|
||||||
elem.c('encryption', {required: 1});
|
|
||||||
var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
|
|
||||||
crypto.forEach(function(line) {
|
|
||||||
elem.c('crypto', SDPUtil.parse_crypto(line)).up();
|
|
||||||
});
|
|
||||||
elem.up(); // end of encryption
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ssrc) {
|
|
||||||
// new style mapping
|
|
||||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
// FIXME: group by ssrc and support multiple different ssrcs
|
|
||||||
var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
|
|
||||||
if(ssrclines.length > 0) {
|
|
||||||
ssrclines.forEach(function (line) {
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var linessrc = line.substr(0, idx).substr(7);
|
|
||||||
if (linessrc != ssrc) {
|
|
||||||
elem.up();
|
|
||||||
ssrc = linessrc;
|
|
||||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
}
|
|
||||||
var kv = line.substr(idx + 1);
|
|
||||||
elem.c('parameter');
|
|
||||||
if (kv.indexOf(':') == -1) {
|
|
||||||
elem.attrs({ name: kv });
|
|
||||||
} else {
|
|
||||||
var k = kv.split(':', 2)[0];
|
|
||||||
elem.attrs({ name: k });
|
|
||||||
|
|
||||||
var v = kv.split(':', 2)[1];
|
|
||||||
v = SDPUtil.filter_special_chars(v);
|
|
||||||
elem.attrs({ value: v });
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
elem.up();
|
|
||||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)});
|
|
||||||
elem.up();
|
|
||||||
var msid = null;
|
|
||||||
if(mline.media == "audio") {
|
|
||||||
msid = APP.RTC.localAudio.getId();
|
|
||||||
} else {
|
|
||||||
msid = APP.RTC.localVideo.getId();
|
|
||||||
}
|
|
||||||
if(msid !== null) {
|
|
||||||
msid = SDPUtil.filter_special_chars(msid);
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "msid", value:msid});
|
|
||||||
elem.up();
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "mslabel", value:msid});
|
|
||||||
elem.up();
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "label", value:msid});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
|
|
||||||
// XEP-0339 handle ssrc-group attributes
|
|
||||||
var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
|
|
||||||
ssrc_group_lines.forEach(function(line) {
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var semantics = line.substr(0, idx).substr(13);
|
|
||||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
|
||||||
if (ssrcs.length) {
|
|
||||||
elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
ssrcs.forEach(function(ssrc) {
|
|
||||||
elem.c('source', { ssrc: ssrc })
|
|
||||||
.up();
|
|
||||||
});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
|
|
||||||
elem.c('rtcp-mux').up();
|
|
||||||
}
|
|
||||||
|
|
||||||
// XEP-0293 -- map a=rtcp-fb:*
|
|
||||||
this.rtcpFbToJingle(i, elem, '*');
|
|
||||||
|
|
||||||
// XEP-0294
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
|
|
||||||
lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
|
|
||||||
for (j = 0; j < lines.length; j++) {
|
|
||||||
tmp = SDPUtil.parse_extmap(lines[j]);
|
|
||||||
elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
|
|
||||||
uri: tmp.uri,
|
|
||||||
id: tmp.value });
|
|
||||||
if (tmp.hasOwnProperty('direction')) {
|
|
||||||
switch (tmp.direction) {
|
|
||||||
case 'sendonly':
|
|
||||||
elem.attrs({senders: 'responder'});
|
|
||||||
break;
|
|
||||||
case 'recvonly':
|
|
||||||
elem.attrs({senders: 'initiator'});
|
|
||||||
break;
|
|
||||||
case 'sendrecv':
|
|
||||||
elem.attrs({senders: 'both'});
|
|
||||||
break;
|
|
||||||
case 'inactive':
|
|
||||||
elem.attrs({senders: 'none'});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: handle params
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elem.up(); // end of description
|
|
||||||
}
|
|
||||||
|
|
||||||
// map ice-ufrag/pwd, dtls fingerprint, candidates
|
|
||||||
this.transportToJingle(i, elem);
|
|
||||||
|
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
|
|
||||||
elem.attrs({senders: 'both'});
|
|
||||||
} else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
|
|
||||||
elem.attrs({senders: 'initiator'});
|
|
||||||
} else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
|
|
||||||
elem.attrs({senders: 'responder'});
|
|
||||||
} else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
|
|
||||||
elem.attrs({senders: 'none'});
|
|
||||||
}
|
|
||||||
if (mline.port == '0') {
|
|
||||||
// estos hack to reject an m-line
|
|
||||||
elem.attrs({senders: 'rejected'});
|
|
||||||
}
|
|
||||||
elem.up(); // end of content
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
return elem;
|
|
||||||
};
|
|
||||||
|
|
||||||
SDP.prototype.transportToJingle = function (mediaindex, elem) {
|
|
||||||
var tmp, sctpmap, sctpAttrs, fingerprints;
|
|
||||||
var self = this;
|
|
||||||
elem.c('transport');
|
|
||||||
|
|
||||||
// XEP-0343 DTLS/SCTP
|
|
||||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
|
|
||||||
{
|
|
||||||
sctpmap = SDPUtil.find_line(
|
|
||||||
this.media[mediaindex], 'a=sctpmap:', self.session);
|
|
||||||
if (sctpmap)
|
|
||||||
{
|
|
||||||
sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
|
|
||||||
elem.c('sctpmap',
|
|
||||||
{
|
|
||||||
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
|
|
||||||
number: sctpAttrs[0], /* SCTP port */
|
|
||||||
protocol: sctpAttrs[1] /* protocol */
|
|
||||||
});
|
|
||||||
// Optional stream count attribute
|
|
||||||
if (sctpAttrs.length > 2)
|
|
||||||
elem.attrs({ streams: sctpAttrs[2]});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// XEP-0320
|
|
||||||
fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
|
||||||
fingerprints.forEach(function(line) {
|
|
||||||
tmp = SDPUtil.parse_fingerprint(line);
|
|
||||||
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
|
|
||||||
elem.c('fingerprint').t(tmp.fingerprint);
|
|
||||||
delete tmp.fingerprint;
|
|
||||||
line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
|
|
||||||
if (line) {
|
|
||||||
tmp.setup = line.substr(8);
|
|
||||||
}
|
|
||||||
elem.attrs(tmp);
|
|
||||||
elem.up(); // end of fingerprint
|
|
||||||
});
|
|
||||||
tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
|
|
||||||
if (tmp) {
|
|
||||||
tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
|
||||||
elem.attrs(tmp);
|
|
||||||
// XEP-0176
|
|
||||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
|
|
||||||
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
|
|
||||||
lines.forEach(function (line) {
|
|
||||||
var candidate = SDPUtil.candidateToJingle(line);
|
|
||||||
var protocol = (candidate &&
|
|
||||||
typeof candidate.protocol === 'string')
|
|
||||||
? candidate.protocol.toLowerCase() : '';
|
|
||||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
|
||||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
elem.c('candidate', candidate).up();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elem.up(); // end of transport
|
|
||||||
};
|
|
||||||
|
|
||||||
SDP.prototype.rtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
|
|
||||||
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
|
|
||||||
lines.forEach(function (line) {
|
|
||||||
var tmp = SDPUtil.parse_rtcpfb(line);
|
|
||||||
if (tmp.type == 'trr-int') {
|
|
||||||
elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
|
|
||||||
elem.up();
|
|
||||||
} else {
|
|
||||||
elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
|
|
||||||
if (tmp.params.length > 0) {
|
|
||||||
elem.attrs({'subtype': tmp.params[0]});
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SDP.prototype.rtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
|
|
||||||
var media = '';
|
|
||||||
var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
|
||||||
if (tmp.length) {
|
|
||||||
media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
|
|
||||||
if (tmp.attr('value')) {
|
|
||||||
media += tmp.attr('value');
|
|
||||||
} else {
|
|
||||||
media += '0';
|
|
||||||
}
|
|
||||||
media += '\r\n';
|
|
||||||
}
|
|
||||||
tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
|
||||||
tmp.each(function () {
|
|
||||||
media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
|
|
||||||
if ($(this).attr('subtype')) {
|
|
||||||
media += ' ' + $(this).attr('subtype');
|
|
||||||
}
|
|
||||||
media += '\r\n';
|
|
||||||
});
|
|
||||||
return media;
|
|
||||||
};
|
|
||||||
|
|
||||||
// construct an SDP from a jingle stanza
|
|
||||||
SDP.prototype.fromJingle = function (jingle) {
|
|
||||||
var self = this;
|
|
||||||
this.raw = 'v=0\r\n' +
|
|
||||||
'o=- 1923518516 2 IN IP4 0.0.0.0\r\n' +// FIXME
|
|
||||||
's=-\r\n' +
|
|
||||||
't=0 0\r\n';
|
|
||||||
// http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
|
|
||||||
if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
|
|
||||||
$(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
|
|
||||||
var contents = $(group).find('>content').map(function (idx, content) {
|
|
||||||
return content.getAttribute('name');
|
|
||||||
}).get();
|
|
||||||
if (contents.length > 0) {
|
|
||||||
self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.session = this.raw;
|
|
||||||
jingle.find('>content').each(function () {
|
|
||||||
var m = self.jingle2media($(this));
|
|
||||||
self.media.push(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
// reconstruct msid-semantic -- apparently not necessary
|
|
||||||
/*
|
|
||||||
var msid = SDPUtil.parse_ssrc(this.raw);
|
|
||||||
if (msid.hasOwnProperty('mslabel')) {
|
|
||||||
this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.raw = this.session + this.media.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
// translate a jingle content element into an an SDP media part
|
|
||||||
SDP.prototype.jingle2media = function (content) {
|
|
||||||
var media = '',
|
|
||||||
desc = content.find('description'),
|
|
||||||
ssrc = desc.attr('ssrc'),
|
|
||||||
self = this,
|
|
||||||
tmp;
|
|
||||||
var sctp = content.find(
|
|
||||||
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
|
||||||
|
|
||||||
tmp = { media: desc.attr('media') };
|
|
||||||
tmp.port = '1';
|
|
||||||
if (content.attr('senders') == 'rejected') {
|
|
||||||
// estos hack to reject an m-line.
|
|
||||||
tmp.port = '0';
|
|
||||||
}
|
|
||||||
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
|
||||||
if (sctp.length)
|
|
||||||
tmp.proto = 'DTLS/SCTP';
|
|
||||||
else
|
|
||||||
tmp.proto = 'RTP/SAVPF';
|
|
||||||
} else {
|
|
||||||
tmp.proto = 'RTP/AVPF';
|
|
||||||
}
|
|
||||||
if (!sctp.length) {
|
|
||||||
tmp.fmt = desc.find('payload-type').map(
|
|
||||||
function () { return this.getAttribute('id'); }).get();
|
|
||||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
|
||||||
} else {
|
|
||||||
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
|
|
||||||
media += 'a=sctpmap:' + sctp.attr('number') +
|
|
||||||
' ' + sctp.attr('protocol');
|
|
||||||
|
|
||||||
var streamCount = sctp.attr('streams');
|
|
||||||
if (streamCount)
|
|
||||||
media += ' ' + streamCount + '\r\n';
|
|
||||||
else
|
|
||||||
media += '\r\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
media += 'c=IN IP4 0.0.0.0\r\n';
|
|
||||||
if (!sctp.length)
|
|
||||||
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
|
|
||||||
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
|
||||||
if (tmp.length) {
|
|
||||||
if (tmp.attr('ufrag')) {
|
|
||||||
media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
|
|
||||||
}
|
|
||||||
if (tmp.attr('pwd')) {
|
|
||||||
media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
|
|
||||||
}
|
|
||||||
tmp.find('>fingerprint').each(function () {
|
|
||||||
// FIXME: check namespace at some point
|
|
||||||
media += 'a=fingerprint:' + this.getAttribute('hash');
|
|
||||||
media += ' ' + $(this).text();
|
|
||||||
media += '\r\n';
|
|
||||||
if (this.getAttribute('setup')) {
|
|
||||||
media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
switch (content.attr('senders')) {
|
|
||||||
case 'initiator':
|
|
||||||
media += 'a=sendonly\r\n';
|
|
||||||
break;
|
|
||||||
case 'responder':
|
|
||||||
media += 'a=recvonly\r\n';
|
|
||||||
break;
|
|
||||||
case 'none':
|
|
||||||
media += 'a=inactive\r\n';
|
|
||||||
break;
|
|
||||||
case 'both':
|
|
||||||
media += 'a=sendrecv\r\n';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
media += 'a=mid:' + content.attr('name') + '\r\n';
|
|
||||||
|
|
||||||
// <description><rtcp-mux/></description>
|
|
||||||
// see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
|
|
||||||
// and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
|
|
||||||
if (desc.find('rtcp-mux').length) {
|
|
||||||
media += 'a=rtcp-mux\r\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desc.find('encryption').length) {
|
|
||||||
desc.find('encryption>crypto').each(function () {
|
|
||||||
media += 'a=crypto:' + this.getAttribute('tag');
|
|
||||||
media += ' ' + this.getAttribute('crypto-suite');
|
|
||||||
media += ' ' + this.getAttribute('key-params');
|
|
||||||
if (this.getAttribute('session-params')) {
|
|
||||||
media += ' ' + this.getAttribute('session-params');
|
|
||||||
}
|
|
||||||
media += '\r\n';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
desc.find('payload-type').each(function () {
|
|
||||||
media += SDPUtil.build_rtpmap(this) + '\r\n';
|
|
||||||
if ($(this).find('>parameter').length) {
|
|
||||||
media += 'a=fmtp:' + this.getAttribute('id') + ' ';
|
|
||||||
media += $(this).find('parameter').map(function () {
|
|
||||||
return (this.getAttribute('name')
|
|
||||||
? (this.getAttribute('name') + '=') : '') +
|
|
||||||
this.getAttribute('value');
|
|
||||||
}).get().join('; ');
|
|
||||||
media += '\r\n';
|
|
||||||
}
|
|
||||||
// xep-0293
|
|
||||||
media += self.rtcpFbFromJingle($(this), this.getAttribute('id'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// xep-0293
|
|
||||||
media += self.rtcpFbFromJingle(desc, '*');
|
|
||||||
|
|
||||||
// xep-0294
|
|
||||||
tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
|
|
||||||
tmp.each(function () {
|
|
||||||
media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
|
|
||||||
var protocol = this.getAttribute('protocol');
|
|
||||||
protocol = (typeof protocol === 'string') ? protocol.toLowerCase(): '';
|
|
||||||
|
|
||||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
|
||||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
media += SDPUtil.candidateFromJingle(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// XEP-0339 handle ssrc-group attributes
|
|
||||||
content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
|
||||||
var semantics = this.getAttribute('semantics');
|
|
||||||
var ssrcs = $(this).find('>source').map(function() {
|
|
||||||
return this.getAttribute('ssrc');
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
if (ssrcs.length) {
|
|
||||||
media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
|
||||||
tmp.each(function () {
|
|
||||||
var ssrc = this.getAttribute('ssrc');
|
|
||||||
$(this).find('>parameter').each(function () {
|
|
||||||
var name = this.getAttribute('name');
|
|
||||||
var value = this.getAttribute('value');
|
|
||||||
value = SDPUtil.filter_special_chars(value);
|
|
||||||
media += 'a=ssrc:' + ssrc + ' ' + name;
|
|
||||||
if (value && value.length)
|
|
||||||
media += ':' + value;
|
|
||||||
media += '\r\n';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return media;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = SDP;
|
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
|
|
||||||
function SDPDiffer(mySDP, otherSDP)
|
|
||||||
{
|
|
||||||
this.mySDP = mySDP;
|
|
||||||
this.otherSDP = otherSDP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns map of MediaChannel that contains media contained in
|
|
||||||
* 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx.
|
|
||||||
*/
|
|
||||||
SDPDiffer.prototype.getNewMedia = function() {
|
|
||||||
|
|
||||||
// this could be useful in Array.prototype.
|
|
||||||
function arrayEquals(array) {
|
|
||||||
// if the other array is a falsy value, return
|
|
||||||
if (!array)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// compare lengths - can save a lot of time
|
|
||||||
if (this.length != array.length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (var i = 0, l=this.length; i < l; i++) {
|
|
||||||
// Check if we have nested arrays
|
|
||||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
|
||||||
// recurse into the nested arrays
|
|
||||||
if (!this[i].equals(array[i]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (this[i] != array[i]) {
|
|
||||||
// Warning - two different object instances will never be
|
|
||||||
// equal: {x:20} != {x:20}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var myMedias = this.mySDP.getMediaSsrcMap();
|
|
||||||
var othersMedias = this.otherSDP.getMediaSsrcMap();
|
|
||||||
var newMedia = {};
|
|
||||||
Object.keys(othersMedias).forEach(function(othersMediaIdx) {
|
|
||||||
var myMedia = myMedias[othersMediaIdx];
|
|
||||||
var othersMedia = othersMedias[othersMediaIdx];
|
|
||||||
if(!myMedia && othersMedia) {
|
|
||||||
// Add whole channel
|
|
||||||
newMedia[othersMediaIdx] = othersMedia;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Look for new ssrcs across the channel
|
|
||||||
Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
|
|
||||||
if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
|
|
||||||
// Allocate channel if we've found ssrc that doesn't exist in
|
|
||||||
// our channel
|
|
||||||
if(!newMedia[othersMediaIdx]){
|
|
||||||
newMedia[othersMediaIdx] = {
|
|
||||||
mediaindex: othersMedia.mediaindex,
|
|
||||||
mid: othersMedia.mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Look for new ssrc groups across the channels
|
|
||||||
othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
|
|
||||||
|
|
||||||
// try to match the other ssrc-group with an ssrc-group of ours
|
|
||||||
var matched = false;
|
|
||||||
for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
|
|
||||||
var mySsrcGroup = myMedia.ssrcGroups[i];
|
|
||||||
if (otherSsrcGroup.semantics == mySsrcGroup.semantics &&
|
|
||||||
arrayEquals.apply(otherSsrcGroup.ssrcs,
|
|
||||||
[mySsrcGroup.ssrcs])) {
|
|
||||||
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
// Allocate channel if we've found an ssrc-group that doesn't
|
|
||||||
// exist in our channel
|
|
||||||
|
|
||||||
if(!newMedia[othersMediaIdx]){
|
|
||||||
newMedia[othersMediaIdx] = {
|
|
||||||
mediaindex: othersMedia.mediaindex,
|
|
||||||
mid: othersMedia.mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newMedia;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: document!
|
|
||||||
*/
|
|
||||||
SDPDiffer.prototype.toJingle = function(modify) {
|
|
||||||
var sdpMediaSsrcs = this.getNewMedia();
|
|
||||||
|
|
||||||
var modified = false;
|
|
||||||
Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
|
|
||||||
modified = true;
|
|
||||||
var media = sdpMediaSsrcs[mediaindex];
|
|
||||||
modify.c('content', {name: media.mid});
|
|
||||||
|
|
||||||
modify.c('description',
|
|
||||||
{xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
|
|
||||||
// FIXME: not completely sure this operates on blocks and / or handles
|
|
||||||
// different ssrcs correctly
|
|
||||||
// generate sources from lines
|
|
||||||
Object.keys(media.ssrcs).forEach(function(ssrcNum) {
|
|
||||||
var mediaSsrc = media.ssrcs[ssrcNum];
|
|
||||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
|
||||||
// iterate over ssrc lines
|
|
||||||
mediaSsrc.lines.forEach(function (line) {
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var kv = line.substr(idx + 1);
|
|
||||||
modify.c('parameter');
|
|
||||||
if (kv.indexOf(':') == -1) {
|
|
||||||
modify.attrs({ name: kv });
|
|
||||||
} else {
|
|
||||||
var nv = kv.split(':', 2);
|
|
||||||
var name = nv[0];
|
|
||||||
var value = SDPUtil.filter_special_chars(nv[1]);
|
|
||||||
modify.attrs({ name: name });
|
|
||||||
modify.attrs({ value: value });
|
|
||||||
}
|
|
||||||
modify.up(); // end of parameter
|
|
||||||
});
|
|
||||||
modify.up(); // end of source
|
|
||||||
});
|
|
||||||
|
|
||||||
// generate source groups from lines
|
|
||||||
media.ssrcGroups.forEach(function(ssrcGroup) {
|
|
||||||
if (ssrcGroup.ssrcs.length) {
|
|
||||||
|
|
||||||
modify.c('ssrc-group', {
|
|
||||||
semantics: ssrcGroup.semantics,
|
|
||||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
|
||||||
});
|
|
||||||
|
|
||||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
|
||||||
modify.c('source', { ssrc: ssrc })
|
|
||||||
.up(); // end of source
|
|
||||||
});
|
|
||||||
modify.up(); // end of ssrc-group
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modify.up(); // end of description
|
|
||||||
modify.up(); // end of content
|
|
||||||
});
|
|
||||||
|
|
||||||
return modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = SDPDiffer;
|
|
|
@ -1,362 +0,0 @@
|
||||||
/* jshint -W101 */
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
|
|
||||||
var SDPUtil = {
|
|
||||||
filter_special_chars: function (text) {
|
|
||||||
return text.replace(/[\\\/\{,\}\+]/g, "");
|
|
||||||
},
|
|
||||||
iceparams: function (mediadesc, sessiondesc) {
|
|
||||||
var data = null;
|
|
||||||
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
|
||||||
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
|
|
||||||
data = {
|
|
||||||
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
|
|
||||||
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_iceufrag: function (line) {
|
|
||||||
return line.substring(12);
|
|
||||||
},
|
|
||||||
build_iceufrag: function (frag) {
|
|
||||||
return 'a=ice-ufrag:' + frag;
|
|
||||||
},
|
|
||||||
parse_icepwd: function (line) {
|
|
||||||
return line.substring(10);
|
|
||||||
},
|
|
||||||
build_icepwd: function (pwd) {
|
|
||||||
return 'a=ice-pwd:' + pwd;
|
|
||||||
},
|
|
||||||
parse_mid: function (line) {
|
|
||||||
return line.substring(6);
|
|
||||||
},
|
|
||||||
parse_mline: function (line) {
|
|
||||||
var parts = line.substring(2).split(' '),
|
|
||||||
data = {};
|
|
||||||
data.media = parts.shift();
|
|
||||||
data.port = parts.shift();
|
|
||||||
data.proto = parts.shift();
|
|
||||||
if (parts[parts.length - 1] === '') { // trailing whitespace
|
|
||||||
parts.pop();
|
|
||||||
}
|
|
||||||
data.fmt = parts;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
build_mline: function (mline) {
|
|
||||||
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
|
|
||||||
},
|
|
||||||
parse_rtpmap: function (line) {
|
|
||||||
var parts = line.substring(9).split(' '),
|
|
||||||
data = {};
|
|
||||||
data.id = parts.shift();
|
|
||||||
parts = parts[0].split('/');
|
|
||||||
data.name = parts.shift();
|
|
||||||
data.clockrate = parts.shift();
|
|
||||||
data.channels = parts.length ? parts.shift() : '1';
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
|
|
||||||
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
|
|
||||||
* @returns [SCTP port number, protocol, streams]
|
|
||||||
*/
|
|
||||||
parse_sctpmap: function (line)
|
|
||||||
{
|
|
||||||
var parts = line.substring(10).split(' ');
|
|
||||||
var sctpPort = parts[0];
|
|
||||||
var protocol = parts[1];
|
|
||||||
// Stream count is optional
|
|
||||||
var streamCount = parts.length > 2 ? parts[2] : null;
|
|
||||||
return [sctpPort, protocol, streamCount];// SCTP port
|
|
||||||
},
|
|
||||||
build_rtpmap: function (el) {
|
|
||||||
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
|
||||||
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
|
||||||
line += '/' + el.getAttribute('channels');
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
},
|
|
||||||
parse_crypto: function (line) {
|
|
||||||
var parts = line.substring(9).split(' '),
|
|
||||||
data = {};
|
|
||||||
data.tag = parts.shift();
|
|
||||||
data['crypto-suite'] = parts.shift();
|
|
||||||
data['key-params'] = parts.shift();
|
|
||||||
if (parts.length) {
|
|
||||||
data['session-params'] = parts.join(' ');
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_fingerprint: function (line) { // RFC 4572
|
|
||||||
var parts = line.substring(14).split(' '),
|
|
||||||
data = {};
|
|
||||||
data.hash = parts.shift();
|
|
||||||
data.fingerprint = parts.shift();
|
|
||||||
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_fmtp: function (line) {
|
|
||||||
var parts = line.split(' '),
|
|
||||||
i, key, value,
|
|
||||||
data = [];
|
|
||||||
parts.shift();
|
|
||||||
parts = parts.join(' ').split(';');
|
|
||||||
for (i = 0; i < parts.length; i++) {
|
|
||||||
key = parts[i].split('=')[0];
|
|
||||||
while (key.length && key[0] == ' ') {
|
|
||||||
key = key.substring(1);
|
|
||||||
}
|
|
||||||
value = parts[i].split('=')[1];
|
|
||||||
if (key && value) {
|
|
||||||
data.push({name: key, value: value});
|
|
||||||
} else if (key) {
|
|
||||||
// rfc 4733 (DTMF) style stuff
|
|
||||||
data.push({name: '', value: key});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_icecandidate: function (line) {
|
|
||||||
var candidate = {},
|
|
||||||
elems = line.split(' ');
|
|
||||||
candidate.foundation = elems[0].substring(12);
|
|
||||||
candidate.component = elems[1];
|
|
||||||
candidate.protocol = elems[2].toLowerCase();
|
|
||||||
candidate.priority = elems[3];
|
|
||||||
candidate.ip = elems[4];
|
|
||||||
candidate.port = elems[5];
|
|
||||||
// elems[6] => "typ"
|
|
||||||
candidate.type = elems[7];
|
|
||||||
candidate.generation = 0; // default value, may be overwritten below
|
|
||||||
for (var i = 8; i < elems.length; i += 2) {
|
|
||||||
switch (elems[i]) {
|
|
||||||
case 'raddr':
|
|
||||||
candidate['rel-addr'] = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'rport':
|
|
||||||
candidate['rel-port'] = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'generation':
|
|
||||||
candidate.generation = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'tcptype':
|
|
||||||
candidate.tcptype = elems[i + 1];
|
|
||||||
break;
|
|
||||||
default: // TODO
|
|
||||||
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
candidate.network = '1';
|
|
||||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
|
||||||
return candidate;
|
|
||||||
},
|
|
||||||
build_icecandidate: function (cand) {
|
|
||||||
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
|
|
||||||
line += ' ';
|
|
||||||
switch (cand.type) {
|
|
||||||
case 'srflx':
|
|
||||||
case 'prflx':
|
|
||||||
case 'relay':
|
|
||||||
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
|
|
||||||
line += 'raddr';
|
|
||||||
line += ' ';
|
|
||||||
line += cand['rel-addr'];
|
|
||||||
line += ' ';
|
|
||||||
line += 'rport';
|
|
||||||
line += ' ';
|
|
||||||
line += cand['rel-port'];
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (cand.hasOwnAttribute('tcptype')) {
|
|
||||||
line += 'tcptype';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.tcptype;
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
line += 'generation';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
|
|
||||||
return line;
|
|
||||||
},
|
|
||||||
parse_ssrc: function (desc) {
|
|
||||||
// proprietary mapping of a=ssrc lines
|
|
||||||
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
|
|
||||||
// and parse according to that
|
|
||||||
var lines = desc.split('\r\n'),
|
|
||||||
data = {};
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].substring(0, 7) == 'a=ssrc:') {
|
|
||||||
var idx = lines[i].indexOf(' ');
|
|
||||||
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_rtcpfb: function (line) {
|
|
||||||
var parts = line.substr(10).split(' ');
|
|
||||||
var data = {};
|
|
||||||
data.pt = parts.shift();
|
|
||||||
data.type = parts.shift();
|
|
||||||
data.params = parts;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
parse_extmap: function (line) {
|
|
||||||
var parts = line.substr(9).split(' ');
|
|
||||||
var data = {};
|
|
||||||
data.value = parts.shift();
|
|
||||||
if (data.value.indexOf('/') != -1) {
|
|
||||||
data.direction = data.value.substr(data.value.indexOf('/') + 1);
|
|
||||||
data.value = data.value.substr(0, data.value.indexOf('/'));
|
|
||||||
} else {
|
|
||||||
data.direction = 'both';
|
|
||||||
}
|
|
||||||
data.uri = parts.shift();
|
|
||||||
data.params = parts;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
find_line: function (haystack, needle, sessionpart) {
|
|
||||||
var lines = haystack.split('\r\n');
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].substring(0, needle.length) == needle) {
|
|
||||||
return lines[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!sessionpart) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// search session part
|
|
||||||
lines = sessionpart.split('\r\n');
|
|
||||||
for (var j = 0; j < lines.length; j++) {
|
|
||||||
if (lines[j].substring(0, needle.length) == needle) {
|
|
||||||
return lines[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
find_lines: function (haystack, needle, sessionpart) {
|
|
||||||
var lines = haystack.split('\r\n'),
|
|
||||||
needles = [];
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].substring(0, needle.length) == needle)
|
|
||||||
needles.push(lines[i]);
|
|
||||||
}
|
|
||||||
if (needles.length || !sessionpart) {
|
|
||||||
return needles;
|
|
||||||
}
|
|
||||||
// search session part
|
|
||||||
lines = sessionpart.split('\r\n');
|
|
||||||
for (var j = 0; j < lines.length; j++) {
|
|
||||||
if (lines[j].substring(0, needle.length) == needle) {
|
|
||||||
needles.push(lines[j]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return needles;
|
|
||||||
},
|
|
||||||
candidateToJingle: function (line) {
|
|
||||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
|
||||||
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
|
||||||
if (line.indexOf('candidate:') === 0) {
|
|
||||||
line = 'a=' + line;
|
|
||||||
} else if (line.substring(0, 12) != 'a=candidate:') {
|
|
||||||
console.log('parseCandidate called with a line that is not a candidate line');
|
|
||||||
console.log(line);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (line.substring(line.length - 2) == '\r\n') // chomp it
|
|
||||||
line = line.substring(0, line.length - 2);
|
|
||||||
var candidate = {},
|
|
||||||
elems = line.split(' '),
|
|
||||||
i;
|
|
||||||
if (elems[6] != 'typ') {
|
|
||||||
console.log('did not find typ in the right place');
|
|
||||||
console.log(line);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
candidate.foundation = elems[0].substring(12);
|
|
||||||
candidate.component = elems[1];
|
|
||||||
candidate.protocol = elems[2].toLowerCase();
|
|
||||||
candidate.priority = elems[3];
|
|
||||||
candidate.ip = elems[4];
|
|
||||||
candidate.port = elems[5];
|
|
||||||
// elems[6] => "typ"
|
|
||||||
candidate.type = elems[7];
|
|
||||||
|
|
||||||
candidate.generation = '0'; // default, may be overwritten below
|
|
||||||
for (i = 8; i < elems.length; i += 2) {
|
|
||||||
switch (elems[i]) {
|
|
||||||
case 'raddr':
|
|
||||||
candidate['rel-addr'] = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'rport':
|
|
||||||
candidate['rel-port'] = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'generation':
|
|
||||||
candidate.generation = elems[i + 1];
|
|
||||||
break;
|
|
||||||
case 'tcptype':
|
|
||||||
candidate.tcptype = elems[i + 1];
|
|
||||||
break;
|
|
||||||
default: // TODO
|
|
||||||
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
candidate.network = '1';
|
|
||||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
|
||||||
return candidate;
|
|
||||||
},
|
|
||||||
candidateFromJingle: function (cand) {
|
|
||||||
var line = 'a=candidate:';
|
|
||||||
line += cand.getAttribute('foundation');
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('component');
|
|
||||||
line += ' ';
|
|
||||||
|
|
||||||
var protocol = cand.getAttribute('protocol');
|
|
||||||
// use tcp candidates for FF
|
|
||||||
if (RTCBrowserType.isFirefox() && protocol.toLowerCase() == 'ssltcp') {
|
|
||||||
protocol = 'tcp';
|
|
||||||
}
|
|
||||||
|
|
||||||
line += protocol; //.toUpperCase(); // chrome M23 doesn't like this
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('priority');
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('ip');
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('port');
|
|
||||||
line += ' ';
|
|
||||||
line += 'typ';
|
|
||||||
line += ' ' + cand.getAttribute('type');
|
|
||||||
line += ' ';
|
|
||||||
switch (cand.getAttribute('type')) {
|
|
||||||
case 'srflx':
|
|
||||||
case 'prflx':
|
|
||||||
case 'relay':
|
|
||||||
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
|
|
||||||
line += 'raddr';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('rel-addr');
|
|
||||||
line += ' ';
|
|
||||||
line += 'rport';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('rel-port');
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (protocol.toLowerCase() == 'tcp') {
|
|
||||||
line += 'tcptype';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('tcptype');
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
line += 'generation';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('generation') || '0';
|
|
||||||
return line + '\r\n';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
module.exports = SDPUtil;
|
|
|
@ -1,492 +0,0 @@
|
||||||
/* global $, config, mozRTCPeerConnection, RTCPeerConnection,
|
|
||||||
webkitRTCPeerConnection, RTCSessionDescription */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var RTC = require('../RTC/RTC');
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
|
|
||||||
function TraceablePeerConnection(ice_config, constraints, session) {
|
|
||||||
var self = this;
|
|
||||||
var RTCPeerConnectionType = null;
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
RTCPeerConnectionType = mozRTCPeerConnection;
|
|
||||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
RTCPeerConnectionType = RTCPeerConnection;
|
|
||||||
} else {
|
|
||||||
RTCPeerConnectionType = webkitRTCPeerConnection;
|
|
||||||
}
|
|
||||||
self.eventEmitter = session.eventEmitter;
|
|
||||||
this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
|
|
||||||
this.updateLog = [];
|
|
||||||
this.stats = {};
|
|
||||||
this.statsinterval = null;
|
|
||||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
|
||||||
var Interop = require('sdp-interop').Interop;
|
|
||||||
this.interop = new Interop();
|
|
||||||
var Simulcast = require('sdp-simulcast');
|
|
||||||
this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
|
|
||||||
|
|
||||||
// override as desired
|
|
||||||
this.trace = function (what, info) {
|
|
||||||
/*console.warn('WTRACE', what, info);
|
|
||||||
if (info && RTCBrowserType.isIExplorer()) {
|
|
||||||
if (info.length > 1024) {
|
|
||||||
console.warn('WTRACE', what, info.substr(1024));
|
|
||||||
}
|
|
||||||
if (info.length > 2048) {
|
|
||||||
console.warn('WTRACE', what, info.substr(2048));
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
self.updateLog.push({
|
|
||||||
time: new Date(),
|
|
||||||
type: what,
|
|
||||||
value: info || ""
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.onicecandidate = null;
|
|
||||||
this.peerconnection.onicecandidate = function (event) {
|
|
||||||
// FIXME: this causes stack overflow with Temasys Plugin
|
|
||||||
if (!RTCBrowserType.isTemasysPluginUsed())
|
|
||||||
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
|
||||||
if (self.onicecandidate !== null) {
|
|
||||||
self.onicecandidate(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.onaddstream = null;
|
|
||||||
this.peerconnection.onaddstream = function (event) {
|
|
||||||
self.trace('onaddstream', event.stream.id);
|
|
||||||
if (self.onaddstream !== null) {
|
|
||||||
self.onaddstream(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.onremovestream = null;
|
|
||||||
this.peerconnection.onremovestream = function (event) {
|
|
||||||
self.trace('onremovestream', event.stream.id);
|
|
||||||
if (self.onremovestream !== null) {
|
|
||||||
self.onremovestream(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.onsignalingstatechange = null;
|
|
||||||
this.peerconnection.onsignalingstatechange = function (event) {
|
|
||||||
self.trace('onsignalingstatechange', self.signalingState);
|
|
||||||
if (self.onsignalingstatechange !== null) {
|
|
||||||
self.onsignalingstatechange(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.oniceconnectionstatechange = null;
|
|
||||||
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
||||||
self.trace('oniceconnectionstatechange', self.iceConnectionState);
|
|
||||||
if (self.oniceconnectionstatechange !== null) {
|
|
||||||
self.oniceconnectionstatechange(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.onnegotiationneeded = null;
|
|
||||||
this.peerconnection.onnegotiationneeded = function (event) {
|
|
||||||
self.trace('onnegotiationneeded');
|
|
||||||
if (self.onnegotiationneeded !== null) {
|
|
||||||
self.onnegotiationneeded(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.ondatachannel = null;
|
|
||||||
this.peerconnection.ondatachannel = function (event) {
|
|
||||||
self.trace('ondatachannel', event);
|
|
||||||
if (self.ondatachannel !== null) {
|
|
||||||
self.ondatachannel(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// XXX: do all non-firefox browsers which we support also support this?
|
|
||||||
if (!RTCBrowserType.isFirefox() && this.maxstats) {
|
|
||||||
this.statsinterval = window.setInterval(function() {
|
|
||||||
self.peerconnection.getStats(function(stats) {
|
|
||||||
var results = stats.result();
|
|
||||||
var now = new Date();
|
|
||||||
for (var i = 0; i < results.length; ++i) {
|
|
||||||
results[i].names().forEach(function (name) {
|
|
||||||
var id = results[i].id + '-' + name;
|
|
||||||
if (!self.stats[id]) {
|
|
||||||
self.stats[id] = {
|
|
||||||
startTime: now,
|
|
||||||
endTime: now,
|
|
||||||
values: [],
|
|
||||||
times: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
self.stats[id].values.push(results[i].stat(name));
|
|
||||||
self.stats[id].times.push(now.getTime());
|
|
||||||
if (self.stats[id].values.length > self.maxstats) {
|
|
||||||
self.stats[id].values.shift();
|
|
||||||
self.stats[id].times.shift();
|
|
||||||
}
|
|
||||||
self.stats[id].endTime = now;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string representation of a SessionDescription object.
|
|
||||||
*/
|
|
||||||
var dumpSDP = function(description) {
|
|
||||||
if (typeof description === 'undefined' || description === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'type: ' + description.type + '\r\n' + description.sdp;
|
|
||||||
};
|
|
||||||
|
|
||||||
var insertRecvOnlySSRC = function (desc) {
|
|
||||||
if (typeof desc !== 'object' || desc === null ||
|
|
||||||
typeof desc.sdp !== 'string') {
|
|
||||||
console.warn('An empty description was passed as an argument.');
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transform = require('sdp-transform');
|
|
||||||
var RandomUtil = require('../util/RandomUtil');
|
|
||||||
|
|
||||||
var session = transform.parse(desc.sdp);
|
|
||||||
if (!Array.isArray(session.media))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var modded = false;
|
|
||||||
session.media.forEach(function (bLine) {
|
|
||||||
if (bLine.direction != 'recvonly')
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
modded = true;
|
|
||||||
if (!Array.isArray(bLine.ssrcs) || bLine.ssrcs.length === 0)
|
|
||||||
{
|
|
||||||
var ssrc = RandomUtil.randomInt(1, 0xffffffff);
|
|
||||||
bLine.ssrcs = [{
|
|
||||||
id: ssrc,
|
|
||||||
attribute: 'cname',
|
|
||||||
value: ['recvonly-', ssrc].join('')
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (!modded) ? desc : new RTCSessionDescription({
|
|
||||||
type: desc.type,
|
|
||||||
sdp: transform.write(session),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a SessionDescription object and returns a "normalized" version.
|
|
||||||
* Currently it only takes care of ordering the a=ssrc lines.
|
|
||||||
*/
|
|
||||||
var normalizePlanB = function(desc) {
|
|
||||||
if (typeof desc !== 'object' || desc === null ||
|
|
||||||
typeof desc.sdp !== 'string') {
|
|
||||||
console.warn('An empty description was passed as an argument.');
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transform = require('sdp-transform');
|
|
||||||
var session = transform.parse(desc.sdp);
|
|
||||||
|
|
||||||
if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
|
|
||||||
Array.isArray(session.media)) {
|
|
||||||
session.media.forEach(function (mLine) {
|
|
||||||
|
|
||||||
// Chrome appears to be picky about the order in which a=ssrc lines
|
|
||||||
// are listed in an m-line when rtx is enabled (and thus there are
|
|
||||||
// a=ssrc-group lines with FID semantics). Specifically if we have
|
|
||||||
// "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
|
|
||||||
// the "a=ssrc:S1" lines, SRD fails.
|
|
||||||
// So, put SSRC which appear as the first SSRC in an FID ssrc-group
|
|
||||||
// first.
|
|
||||||
var firstSsrcs = [];
|
|
||||||
var newSsrcLines = [];
|
|
||||||
|
|
||||||
if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
|
|
||||||
mLine.ssrcGroups.forEach(function (group) {
|
|
||||||
if (typeof group.semantics !== 'undefined' &&
|
|
||||||
group.semantics === 'FID') {
|
|
||||||
if (typeof group.ssrcs !== 'undefined') {
|
|
||||||
firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
|
|
||||||
var i;
|
|
||||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
|
||||||
if (typeof mLine.ssrcs[i] === 'object'
|
|
||||||
&& typeof mLine.ssrcs[i].id !== 'undefined'
|
|
||||||
&& !$.inArray(mLine.ssrcs[i].id, firstSsrcs)) {
|
|
||||||
newSsrcLines.push(mLine.ssrcs[i]);
|
|
||||||
delete mLine.ssrcs[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
|
||||||
if (typeof mLine.ssrcs[i] !== 'undefined') {
|
|
||||||
newSsrcLines.push(mLine.ssrcs[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mLine.ssrcs = newSsrcLines;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var resStr = transform.write(session);
|
|
||||||
return new RTCSessionDescription({
|
|
||||||
type: desc.type,
|
|
||||||
sdp: resStr
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
|
|
||||||
TraceablePeerConnection.prototype.__defineGetter__(
|
|
||||||
'signalingState',
|
|
||||||
function() { return this.peerconnection.signalingState; });
|
|
||||||
TraceablePeerConnection.prototype.__defineGetter__(
|
|
||||||
'iceConnectionState',
|
|
||||||
function() { return this.peerconnection.iceConnectionState; });
|
|
||||||
TraceablePeerConnection.prototype.__defineGetter__(
|
|
||||||
'localDescription',
|
|
||||||
function() {
|
|
||||||
var desc = this.peerconnection.localDescription;
|
|
||||||
|
|
||||||
this.trace('getLocalDescription::preTransform', dumpSDP(desc));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
desc = this.interop.toPlanB(desc);
|
|
||||||
this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
|
|
||||||
}
|
|
||||||
return desc;
|
|
||||||
});
|
|
||||||
TraceablePeerConnection.prototype.__defineGetter__(
|
|
||||||
'remoteDescription',
|
|
||||||
function() {
|
|
||||||
var desc = this.peerconnection.remoteDescription;
|
|
||||||
this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
desc = this.interop.toPlanB(desc);
|
|
||||||
this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
|
|
||||||
}
|
|
||||||
return desc;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.addStream = function (stream) {
|
|
||||||
this.trace('addStream', stream.id);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.peerconnection.addStream(stream);
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
|
|
||||||
this.trace('removeStream', stream.id);
|
|
||||||
if(stopStreams) {
|
|
||||||
RTC.stopMediaStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// FF doesn't support this yet.
|
|
||||||
if (this.peerconnection.removeStream)
|
|
||||||
this.peerconnection.removeStream(stream);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
|
||||||
this.trace('createDataChannel', label, opts);
|
|
||||||
return this.peerconnection.createDataChannel(label, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.setLocalDescription
|
|
||||||
= function (description, successCallback, failureCallback) {
|
|
||||||
this.trace('setLocalDescription::preTransform', dumpSDP(description));
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
description = this.interop.toUnifiedPlan(description);
|
|
||||||
this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
this.peerconnection.setLocalDescription(description,
|
|
||||||
function () {
|
|
||||||
self.trace('setLocalDescriptionOnSuccess');
|
|
||||||
successCallback();
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
self.trace('setLocalDescriptionOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, err, self.peerconnection);
|
|
||||||
failureCallback(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
/*
|
|
||||||
if (this.statsinterval === null && this.maxstats > 0) {
|
|
||||||
// start gathering stats
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.setRemoteDescription
|
|
||||||
= function (description, successCallback, failureCallback) {
|
|
||||||
this.trace('setRemoteDescription::preTransform', dumpSDP(description));
|
|
||||||
// TODO the focus should squeze or explode the remote simulcast
|
|
||||||
description = this.simulcast.mungeRemoteDescription(description);
|
|
||||||
this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
description = this.interop.toUnifiedPlan(description);
|
|
||||||
this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.usesPlanB()) {
|
|
||||||
description = normalizePlanB(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
this.peerconnection.setRemoteDescription(description,
|
|
||||||
function () {
|
|
||||||
self.trace('setRemoteDescriptionOnSuccess');
|
|
||||||
successCallback();
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
self.trace('setRemoteDescriptionOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, err, self.peerconnection);
|
|
||||||
failureCallback(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
/*
|
|
||||||
if (this.statsinterval === null && this.maxstats > 0) {
|
|
||||||
// start gathering stats
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.close = function () {
|
|
||||||
this.trace('stop');
|
|
||||||
if (this.statsinterval !== null) {
|
|
||||||
window.clearInterval(this.statsinterval);
|
|
||||||
this.statsinterval = null;
|
|
||||||
}
|
|
||||||
this.peerconnection.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.createOffer
|
|
||||||
= function (successCallback, failureCallback, constraints) {
|
|
||||||
var self = this;
|
|
||||||
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
|
|
||||||
this.peerconnection.createOffer(
|
|
||||||
function (offer) {
|
|
||||||
self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer));
|
|
||||||
// NOTE this is not tested because in meet the focus generates the
|
|
||||||
// offer.
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
offer = self.interop.toPlanB(offer);
|
|
||||||
self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.isChrome())
|
|
||||||
{
|
|
||||||
offer = insertRecvOnlySSRC(offer);
|
|
||||||
self.trace('createOfferOnSuccess::mungeLocalVideoSSRC', dumpSDP(offer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
|
||||||
offer = self.simulcast.mungeLocalDescription(offer);
|
|
||||||
self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer));
|
|
||||||
}
|
|
||||||
successCallback(offer);
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
self.trace('createOfferOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.CREATE_OFFER_FAILED, err, self.peerconnection);
|
|
||||||
failureCallback(err);
|
|
||||||
},
|
|
||||||
constraints
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.createAnswer
|
|
||||||
= function (successCallback, failureCallback, constraints) {
|
|
||||||
var self = this;
|
|
||||||
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
|
|
||||||
this.peerconnection.createAnswer(
|
|
||||||
function (answer) {
|
|
||||||
self.trace('createAnswerOnSuccess::preTransform', dumpSDP(answer));
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
answer = self.interop.toPlanB(answer);
|
|
||||||
self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.isChrome())
|
|
||||||
{
|
|
||||||
answer = insertRecvOnlySSRC(answer);
|
|
||||||
self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC', dumpSDP(answer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
|
||||||
answer = self.simulcast.mungeLocalDescription(answer);
|
|
||||||
self.trace('createAnswerOnSuccess::postTransform (simulcast)', dumpSDP(answer));
|
|
||||||
}
|
|
||||||
successCallback(answer);
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
self.trace('createAnswerOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err, self.peerconnection);
|
|
||||||
failureCallback(err);
|
|
||||||
},
|
|
||||||
constraints
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.addIceCandidate
|
|
||||||
= function (candidate, successCallback, failureCallback) {
|
|
||||||
//var self = this;
|
|
||||||
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
|
|
||||||
this.peerconnection.addIceCandidate(candidate);
|
|
||||||
/* maybe later
|
|
||||||
this.peerconnection.addIceCandidate(candidate,
|
|
||||||
function () {
|
|
||||||
self.trace('addIceCandidateOnSuccess');
|
|
||||||
successCallback();
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
self.trace('addIceCandidateOnFailure', err);
|
|
||||||
failureCallback(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
|
|
||||||
// TODO: Is this the correct way to handle Opera, Temasys?
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
// ignore for now...
|
|
||||||
if(!errback)
|
|
||||||
errback = function () {};
|
|
||||||
this.peerconnection.getStats(null, callback, errback);
|
|
||||||
} else {
|
|
||||||
this.peerconnection.getStats(callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TraceablePeerConnection;
|
|
|
@ -1,442 +0,0 @@
|
||||||
/* global $, $iq, APP, config, messageHandler,
|
|
||||||
roomName, sessionTerminated, Strophe, Util */
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var Settings = require("../settings/Settings");
|
|
||||||
|
|
||||||
var AuthenticationEvents
|
|
||||||
= require("../../service/authentication/AuthenticationEvents");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains logic responsible for enabling/disabling functionality available
|
|
||||||
* only to moderator users.
|
|
||||||
*/
|
|
||||||
var connection = null;
|
|
||||||
var focusUserJid;
|
|
||||||
|
|
||||||
function createExpBackoffTimer(step) {
|
|
||||||
var count = 1;
|
|
||||||
return function (reset) {
|
|
||||||
// Reset call
|
|
||||||
if (reset) {
|
|
||||||
count = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Calculate next timeout
|
|
||||||
var timeout = Math.pow(2, count - 1);
|
|
||||||
count += 1;
|
|
||||||
return timeout * step;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var getNextTimeout = createExpBackoffTimer(1000);
|
|
||||||
var getNextErrorTimeout = createExpBackoffTimer(1000);
|
|
||||||
// External authentication stuff
|
|
||||||
var externalAuthEnabled = false;
|
|
||||||
// Sip gateway can be enabled by configuring Jigasi host in config.js or
|
|
||||||
// it will be enabled automatically if focus detects the component through
|
|
||||||
// service discovery.
|
|
||||||
var sipGatewayEnabled;
|
|
||||||
|
|
||||||
var eventEmitter = null;
|
|
||||||
|
|
||||||
var Moderator = {
|
|
||||||
isModerator: function () {
|
|
||||||
return connection && connection.emuc.isModerator();
|
|
||||||
},
|
|
||||||
|
|
||||||
isPeerModerator: function (peerJid) {
|
|
||||||
return connection &&
|
|
||||||
connection.emuc.getMemberRole(peerJid) === 'moderator';
|
|
||||||
},
|
|
||||||
|
|
||||||
isExternalAuthEnabled: function () {
|
|
||||||
return externalAuthEnabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
isSipGatewayEnabled: function () {
|
|
||||||
return sipGatewayEnabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
setConnection: function (con) {
|
|
||||||
connection = con;
|
|
||||||
},
|
|
||||||
|
|
||||||
init: function (xmpp, emitter) {
|
|
||||||
this.xmppService = xmpp;
|
|
||||||
eventEmitter = emitter;
|
|
||||||
|
|
||||||
sipGatewayEnabled =
|
|
||||||
config.hosts && config.hosts.call_control !== undefined;
|
|
||||||
|
|
||||||
// Message listener that talks to POPUP window
|
|
||||||
function listener(event) {
|
|
||||||
if (event.data && event.data.sessionId) {
|
|
||||||
if (event.origin !== window.location.origin) {
|
|
||||||
console.warn("Ignoring sessionId from different origin: " +
|
|
||||||
event.origin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localStorage.setItem('sessionId', event.data.sessionId);
|
|
||||||
// After popup is closed we will authenticate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Register
|
|
||||||
if (window.addEventListener) {
|
|
||||||
window.addEventListener("message", listener, false);
|
|
||||||
} else {
|
|
||||||
window.attachEvent("onmessage", listener);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onMucMemberLeft: function (jid) {
|
|
||||||
console.info("Someone left is it focus ? " + jid);
|
|
||||||
var resource = Strophe.getResourceFromJid(jid);
|
|
||||||
if (resource === 'focus' && !this.xmppService.sessionTerminated) {
|
|
||||||
console.info(
|
|
||||||
"Focus has left the room - leaving conference");
|
|
||||||
//hangUp();
|
|
||||||
// We'd rather reload to have everything re-initialized
|
|
||||||
// FIXME: show some message before reload
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setFocusUserJid: function (focusJid) {
|
|
||||||
if (!focusUserJid) {
|
|
||||||
focusUserJid = focusJid;
|
|
||||||
console.info("Focus jid set to: " + focusUserJid);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getFocusUserJid: function () {
|
|
||||||
return focusUserJid;
|
|
||||||
},
|
|
||||||
|
|
||||||
getFocusComponent: function () {
|
|
||||||
// Get focus component address
|
|
||||||
var focusComponent = config.hosts.focus;
|
|
||||||
// If not specified use default: 'focus.domain'
|
|
||||||
if (!focusComponent) {
|
|
||||||
focusComponent = 'focus.' + config.hosts.domain;
|
|
||||||
}
|
|
||||||
return focusComponent;
|
|
||||||
},
|
|
||||||
|
|
||||||
createConferenceIq: function (roomName) {
|
|
||||||
// Generate create conference IQ
|
|
||||||
var elem = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
|
||||||
|
|
||||||
// Session Id used for authentication
|
|
||||||
var sessionId = localStorage.getItem('sessionId');
|
|
||||||
var machineUID = Settings.getSettings().uid;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Session ID: " + sessionId + " machine UID: " + machineUID);
|
|
||||||
|
|
||||||
elem.c('conference', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': machineUID
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
elem.attrs({ 'session-id': sessionId});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.hosts.bridge !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'bridge', value: config.hosts.bridge})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enforcedBridge) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'enforcedBridge', value: config.enforcedBridge})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the focus we have Jigasi configured
|
|
||||||
if (config.hosts.call_control !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'call_control', value: config.hosts.call_control})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.channelLastN !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'channelLastN', value: config.channelLastN})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.adaptiveLastN !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'adaptiveLastN', value: config.adaptiveLastN})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.adaptiveSimulcast !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.openSctp !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'openSctp', value: config.openSctp})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if(config.startAudioMuted !== undefined)
|
|
||||||
{
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'startAudioMuted', value: config.startAudioMuted})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if(config.startVideoMuted !== undefined)
|
|
||||||
{
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'startVideoMuted', value: config.startVideoMuted})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'simulcastMode', value: 'rewriting'})
|
|
||||||
.up();
|
|
||||||
elem.up();
|
|
||||||
return elem;
|
|
||||||
},
|
|
||||||
|
|
||||||
parseSessionId: function (resultIq) {
|
|
||||||
var sessionId = $(resultIq).find('conference').attr('session-id');
|
|
||||||
if (sessionId) {
|
|
||||||
console.info('Received sessionId: ' + sessionId);
|
|
||||||
localStorage.setItem('sessionId', sessionId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseConfigOptions: function (resultIq) {
|
|
||||||
|
|
||||||
Moderator.setFocusUserJid(
|
|
||||||
$(resultIq).find('conference').attr('focusjid'));
|
|
||||||
|
|
||||||
var authenticationEnabled
|
|
||||||
= $(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'authentication\'][value=\'true\']').length > 0;
|
|
||||||
|
|
||||||
console.info("Authentication enabled: " + authenticationEnabled);
|
|
||||||
|
|
||||||
externalAuthEnabled = $(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'externalAuth\'][value=\'true\']').length > 0;
|
|
||||||
|
|
||||||
console.info('External authentication enabled: ' + externalAuthEnabled);
|
|
||||||
|
|
||||||
if (!externalAuthEnabled) {
|
|
||||||
// We expect to receive sessionId in 'internal' authentication mode
|
|
||||||
Moderator.parseSessionId(resultIq);
|
|
||||||
}
|
|
||||||
|
|
||||||
var authIdentity = $(resultIq).find('>conference').attr('identity');
|
|
||||||
|
|
||||||
eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
|
|
||||||
authenticationEnabled, authIdentity);
|
|
||||||
|
|
||||||
// Check if focus has auto-detected Jigasi component(this will be also
|
|
||||||
// included if we have passed our host from the config)
|
|
||||||
if ($(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
|
|
||||||
sipGatewayEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("Sip gateway enabled: " + sipGatewayEnabled);
|
|
||||||
},
|
|
||||||
|
|
||||||
// FIXME: we need to show the fact that we're waiting for the focus
|
|
||||||
// to the user(or that focus is not available)
|
|
||||||
allocateConferenceFocus: function (roomName, callback) {
|
|
||||||
// Try to use focus user JID from the config
|
|
||||||
Moderator.setFocusUserJid(config.focusUserJid);
|
|
||||||
// Send create conference IQ
|
|
||||||
var iq = Moderator.createConferenceIq(roomName);
|
|
||||||
var self = this;
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
|
|
||||||
// Setup config options
|
|
||||||
Moderator.parseConfigOptions(result);
|
|
||||||
|
|
||||||
if ('true' === $(result).find('conference').attr('ready')) {
|
|
||||||
// Reset both timers
|
|
||||||
getNextTimeout(true);
|
|
||||||
getNextErrorTimeout(true);
|
|
||||||
// Exec callback
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
var waitMs = getNextTimeout();
|
|
||||||
console.info("Waiting for the focus... " + waitMs);
|
|
||||||
// Reset error timeout
|
|
||||||
getNextErrorTimeout(true);
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(
|
|
||||||
roomName, callback);
|
|
||||||
}, waitMs);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
// Invalid session ? remove and try again
|
|
||||||
// without session ID to get a new one
|
|
||||||
var invalidSession
|
|
||||||
= $(error).find('>error>session-invalid').length;
|
|
||||||
if (invalidSession) {
|
|
||||||
console.info("Session expired! - removing");
|
|
||||||
localStorage.removeItem("sessionId");
|
|
||||||
}
|
|
||||||
if ($(error).find('>error>graceful-shutdown').length) {
|
|
||||||
eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for error returned by the reservation system
|
|
||||||
var reservationErr = $(error).find('>error>reservation-error');
|
|
||||||
if (reservationErr.length) {
|
|
||||||
// Trigger error event
|
|
||||||
var errorCode = reservationErr.attr('error-code');
|
|
||||||
var errorMsg;
|
|
||||||
if ($(error).find('>error>text')) {
|
|
||||||
errorMsg = $(error).find('>error>text').text();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(
|
|
||||||
XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Not authorized to create new room
|
|
||||||
if ($(error).find('>error>not-authorized').length) {
|
|
||||||
console.warn("Unauthorized to start the conference", error);
|
|
||||||
var toDomain
|
|
||||||
= Strophe.getDomainFromJid(error.getAttribute('to'));
|
|
||||||
if (toDomain !== config.hosts.anonymousdomain) {
|
|
||||||
// FIXME: "is external" should come either from
|
|
||||||
// the focus or config.js
|
|
||||||
externalAuthEnabled = true;
|
|
||||||
}
|
|
||||||
eventEmitter.emit(
|
|
||||||
XMPPEvents.AUTHENTICATION_REQUIRED,
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(
|
|
||||||
roomName, callback);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var waitMs = getNextErrorTimeout();
|
|
||||||
console.error("Focus error, retry after " + waitMs, error);
|
|
||||||
// Show message
|
|
||||||
var focusComponent = Moderator.getFocusComponent();
|
|
||||||
var retrySec = waitMs / 1000;
|
|
||||||
// FIXME: message is duplicated ?
|
|
||||||
// Do not show in case of session invalid
|
|
||||||
// which means just a retry
|
|
||||||
if (!invalidSession) {
|
|
||||||
eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED,
|
|
||||||
focusComponent, retrySec);
|
|
||||||
}
|
|
||||||
// Reset response timeout
|
|
||||||
getNextTimeout(true);
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(roomName, callback);
|
|
||||||
}, waitMs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
getLoginUrl: function (roomName, urlCallback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
|
||||||
iq.c('login-url', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': Settings.getSettings().uid
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var url = $(result).find('login-url').attr('url');
|
|
||||||
url = url = decodeURIComponent(url);
|
|
||||||
if (url) {
|
|
||||||
console.info("Got auth url: " + url);
|
|
||||||
urlCallback(url);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Failed to get auth url from the focus", result);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Get auth url error", error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getPopupLoginUrl: function (roomName, urlCallback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
|
||||||
iq.c('login-url', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': Settings.getSettings().uid,
|
|
||||||
popup: true
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var url = $(result).find('login-url').attr('url');
|
|
||||||
url = url = decodeURIComponent(url);
|
|
||||||
if (url) {
|
|
||||||
console.info("Got POPUP auth url: " + url);
|
|
||||||
urlCallback(url);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Failed to get POPUP auth url from the focus", result);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error('Get POPUP auth url error', error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
logout: function (callback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
|
||||||
var sessionId = localStorage.getItem('sessionId');
|
|
||||||
if (!sessionId) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
iq.c('logout', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
'session-id': sessionId
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var logoutUrl = $(result).find('logout').attr('logout-url');
|
|
||||||
if (logoutUrl) {
|
|
||||||
logoutUrl = decodeURIComponent(logoutUrl);
|
|
||||||
}
|
|
||||||
console.info("Log out OK, url: " + logoutUrl, result);
|
|
||||||
localStorage.removeItem('sessionId');
|
|
||||||
callback(logoutUrl);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Logout error", error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Moderator;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
/* global $, $iq, config, connection, focusMucJid, messageHandler,
|
|
||||||
Toolbar, Util */
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
|
|
||||||
|
|
||||||
var recordingToken = null;
|
|
||||||
var recordingEnabled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to use a jirecon component for recording, or use the videobridge
|
|
||||||
* through COLIBRI.
|
|
||||||
*/
|
|
||||||
var useJirecon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID of the jirecon recording session. Jirecon generates it when we
|
|
||||||
* initially start recording, and it needs to be used in subsequent requests
|
|
||||||
* to jirecon.
|
|
||||||
*/
|
|
||||||
var jireconRid = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The callback to update the recording button. Currently used from colibri
|
|
||||||
* after receiving a pending status.
|
|
||||||
*/
|
|
||||||
var recordingStateChangeCallback = null;
|
|
||||||
|
|
||||||
function setRecordingToken(token) {
|
|
||||||
recordingToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRecordingJirecon(state, token, callback, connection) {
|
|
||||||
if (state == recordingEnabled){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var iq = $iq({to: config.hosts.jirecon, type: 'set'})
|
|
||||||
.c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
|
|
||||||
action: (state === 'on') ? 'start' : 'stop',
|
|
||||||
mucjid: connection.emuc.roomjid});
|
|
||||||
if (state === 'off'){
|
|
||||||
iq.attrs({rid: jireconRid});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Start recording');
|
|
||||||
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
// TODO wait for an IQ with the real status, since this is
|
|
||||||
// provisional?
|
|
||||||
jireconRid = $(result).find('recording').attr('rid');
|
|
||||||
console.log('Recording ' +
|
|
||||||
((state === 'on') ? 'started' : 'stopped') +
|
|
||||||
'(jirecon)' + result);
|
|
||||||
recordingEnabled = state;
|
|
||||||
if (state === 'off'){
|
|
||||||
jireconRid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(state);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('Failed to start recording, error: ', error);
|
|
||||||
callback(recordingEnabled);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends a COLIBRI message which enables or disables (according to 'state')
|
|
||||||
// the recording on the bridge. Waits for the result IQ and calls 'callback'
|
|
||||||
// with the new recording state, according to the IQ.
|
|
||||||
function setRecordingColibri(state, token, callback, connection) {
|
|
||||||
var elem = $iq({to: connection.emuc.focusMucJid, type: 'set'});
|
|
||||||
elem.c('conference', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/colibri'
|
|
||||||
});
|
|
||||||
elem.c('recording', {state: state, token: token});
|
|
||||||
|
|
||||||
connection.sendIQ(elem,
|
|
||||||
function (result) {
|
|
||||||
console.log('Set recording "', state, '". Result:', result);
|
|
||||||
var recordingElem = $(result).find('>conference>recording');
|
|
||||||
var newState = recordingElem.attr('state');
|
|
||||||
|
|
||||||
recordingEnabled = newState;
|
|
||||||
callback(newState);
|
|
||||||
|
|
||||||
if (newState === 'pending' && !recordingStateChangeCallback) {
|
|
||||||
recordingStateChangeCallback = callback;
|
|
||||||
connection.addHandler(function(iq){
|
|
||||||
var state = $(iq).find('recording').attr('state');
|
|
||||||
if (state)
|
|
||||||
recordingStateChangeCallback(state);
|
|
||||||
}, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.warn(error);
|
|
||||||
callback(recordingEnabled);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRecording(state, token, callback, connection) {
|
|
||||||
if (useJirecon){
|
|
||||||
setRecordingJirecon(state, token, callback, connection);
|
|
||||||
} else {
|
|
||||||
setRecordingColibri(state, token, callback, connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var Recording = {
|
|
||||||
init: function () {
|
|
||||||
useJirecon = config.hosts &&
|
|
||||||
(typeof config.hosts.jirecon != "undefined");
|
|
||||||
},
|
|
||||||
toggleRecording: function (tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback,
|
|
||||||
connection) {
|
|
||||||
if (!Moderator.isModerator()) {
|
|
||||||
console.log(
|
|
||||||
'non-focus, or conference not yet organized:' +
|
|
||||||
' not enabling recording');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
// Jirecon does not (currently) support a token.
|
|
||||||
if (!recordingToken && !useJirecon) {
|
|
||||||
tokenEmptyCallback(function (value) {
|
|
||||||
setRecordingToken(value);
|
|
||||||
self.toggleRecording(tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback,
|
|
||||||
connection);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldState = recordingEnabled;
|
|
||||||
var newState = (oldState === 'off' || !oldState) ? 'on' : 'off';
|
|
||||||
|
|
||||||
setRecording(newState,
|
|
||||||
recordingToken,
|
|
||||||
function (state) {
|
|
||||||
console.log("New recording state: ", state);
|
|
||||||
if (state === oldState) {
|
|
||||||
// FIXME: new focus:
|
|
||||||
// this will not work when moderator changes
|
|
||||||
// during active session. Then it will assume that
|
|
||||||
// recording status has changed to true, but it might have
|
|
||||||
// been already true(and we only received actual status from
|
|
||||||
// the focus).
|
|
||||||
//
|
|
||||||
// SO we start with status null, so that it is initialized
|
|
||||||
// here and will fail only after second click, so if invalid
|
|
||||||
// token was used we have to press the button twice before
|
|
||||||
// current status will be fetched and token will be reset.
|
|
||||||
//
|
|
||||||
// Reliable way would be to return authentication error.
|
|
||||||
// Or status update when moderator connects.
|
|
||||||
// Or we have to stop recording session when current
|
|
||||||
// moderator leaves the room.
|
|
||||||
|
|
||||||
// Failed to change, reset the token because it might
|
|
||||||
// have been wrong
|
|
||||||
setRecordingToken(null);
|
|
||||||
}
|
|
||||||
recordingStateChangeCallback(state);
|
|
||||||
|
|
||||||
},
|
|
||||||
connection
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Recording;
|
|
|
@ -1,702 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
/* a simple MUC connection plugin
|
|
||||||
* can only handle a single MUC room
|
|
||||||
*/
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
|
|
||||||
module.exports = function(XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('emuc', {
|
|
||||||
connection: null,
|
|
||||||
roomjid: null,
|
|
||||||
myroomjid: null,
|
|
||||||
members: {},
|
|
||||||
list_members: [], // so we can elect a new focus
|
|
||||||
presMap: {},
|
|
||||||
preziMap: {},
|
|
||||||
lastPresenceMap: {},
|
|
||||||
joined: false,
|
|
||||||
isOwner: false,
|
|
||||||
role: null,
|
|
||||||
focusMucJid: null,
|
|
||||||
bridgeIsDown: false,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
},
|
|
||||||
initPresenceMap: function (myroomjid) {
|
|
||||||
this.presMap['to'] = myroomjid;
|
|
||||||
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
|
|
||||||
if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted()) {
|
|
||||||
this.addAudioInfoToPresence(true);
|
|
||||||
}
|
|
||||||
if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted()) {
|
|
||||||
this.addVideoInfoToPresence(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doJoin: function (jid, password) {
|
|
||||||
this.myroomjid = jid;
|
|
||||||
|
|
||||||
console.info("Joined MUC as " + this.myroomjid);
|
|
||||||
|
|
||||||
this.initPresenceMap(this.myroomjid);
|
|
||||||
|
|
||||||
if (!this.roomjid) {
|
|
||||||
this.roomjid = Strophe.getBareJidFromJid(jid);
|
|
||||||
// add handlers (just once)
|
|
||||||
this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
|
|
||||||
}
|
|
||||||
if (password !== undefined) {
|
|
||||||
this.presMap['password'] = password;
|
|
||||||
}
|
|
||||||
this.sendPresence();
|
|
||||||
},
|
|
||||||
doLeave: function () {
|
|
||||||
console.log("do leave", this.myroomjid);
|
|
||||||
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
|
|
||||||
this.presMap.length = 0;
|
|
||||||
this.connection.send(pres);
|
|
||||||
},
|
|
||||||
createNonAnonymousRoom: function () {
|
|
||||||
// http://xmpp.org/extensions/xep-0045.html#createroom-reserved
|
|
||||||
|
|
||||||
var getForm = $iq({type: 'get', to: this.roomjid})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
|
|
||||||
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.connection.sendIQ(getForm, function (form) {
|
|
||||||
|
|
||||||
if (!$(form).find(
|
|
||||||
'>query>x[xmlns="jabber:x:data"]' +
|
|
||||||
'>field[var="muc#roomconfig_whois"]').length) {
|
|
||||||
|
|
||||||
console.error('non-anonymous rooms not supported');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var formSubmit = $iq({to: this.roomjid, type: 'set'})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
|
|
||||||
|
|
||||||
formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
|
|
||||||
formSubmit.c('field', {'var': 'FORM_TYPE'})
|
|
||||||
.c('value')
|
|
||||||
.t('http://jabber.org/protocol/muc#roomconfig').up().up();
|
|
||||||
|
|
||||||
formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
|
|
||||||
.c('value').t('anyone').up().up();
|
|
||||||
|
|
||||||
self.connection.sendIQ(formSubmit);
|
|
||||||
|
|
||||||
}, function (error) {
|
|
||||||
console.error("Error getting room configuration form");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPresence: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
|
|
||||||
// What is this for? A workaround for something?
|
|
||||||
if (pres.getAttribute('type')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse etherpad tag.
|
|
||||||
var etherpad = $(pres).find('>etherpad');
|
|
||||||
if (etherpad.length) {
|
|
||||||
if (config.etherpad_base) {
|
|
||||||
eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url;
|
|
||||||
// Parse prezi tag.
|
|
||||||
var presentation = $(pres).find('>prezi');
|
|
||||||
if (presentation.length) {
|
|
||||||
url = presentation.attr('url');
|
|
||||||
var current = presentation.find('>current').text();
|
|
||||||
|
|
||||||
console.log('presentation info received from', from, url);
|
|
||||||
|
|
||||||
if (this.preziMap[from] == null) {
|
|
||||||
this.preziMap[from] = url;
|
|
||||||
|
|
||||||
$(document).trigger('presentationadded.muc', [from, url, current]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$(document).trigger('gotoslide.muc', [from, url, current]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (this.preziMap[from] != null) {
|
|
||||||
url = this.preziMap[from];
|
|
||||||
delete this.preziMap[from];
|
|
||||||
$(document).trigger('presentationremoved.muc', [from, url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the last presence for participant
|
|
||||||
this.lastPresenceMap[from] = {};
|
|
||||||
|
|
||||||
// Parse audio info tag.
|
|
||||||
var audioMuted = $(pres).find('>audiomuted');
|
|
||||||
if (audioMuted.length) {
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
|
|
||||||
from, (audioMuted.text() === "true"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse video info tag.
|
|
||||||
var videoMuted = $(pres).find('>videomuted');
|
|
||||||
if (videoMuted.length) {
|
|
||||||
var value = (videoMuted.text() === "true");
|
|
||||||
this.lastPresenceMap[from].videoMuted = value;
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_MUTED, from, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var startMuted = $(pres).find('>startmuted');
|
|
||||||
if (startMuted.length && Moderator.isPeerModerator(from)) {
|
|
||||||
eventEmitter.emit(XMPPEvents.START_MUTED_SETTING_CHANGED,
|
|
||||||
startMuted.attr("audio") === "true",
|
|
||||||
startMuted.attr("video") === "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
var devices = $(pres).find('>devices');
|
|
||||||
if(devices.length)
|
|
||||||
{
|
|
||||||
var audio = devices.find('>audio');
|
|
||||||
var video = devices.find('>video');
|
|
||||||
var devicesValues = {audio: false, video: false};
|
|
||||||
if(audio.length && audio.text() === "true")
|
|
||||||
{
|
|
||||||
devicesValues.audio = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(video.length && video.text() === "true")
|
|
||||||
{
|
|
||||||
devicesValues.video = true;
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.DEVICE_AVAILABLE,
|
|
||||||
Strophe.getResourceFromJid(from), devicesValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoType = $(pres).find('>videoType');
|
|
||||||
if (videoType.length)
|
|
||||||
{
|
|
||||||
if (videoType.text().length)
|
|
||||||
{
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
|
|
||||||
Strophe.getResourceFromJid(from), videoType.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats = $(pres).find('>stats');
|
|
||||||
if (stats.length) {
|
|
||||||
var statsObj = {};
|
|
||||||
Strophe.forEachChild(stats[0], "stat", function (el) {
|
|
||||||
statsObj[el.getAttribute("name")] = el.getAttribute("value");
|
|
||||||
});
|
|
||||||
eventEmitter.emit(XMPPEvents.REMOTE_STATS, from, statsObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse status.
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
|
|
||||||
this.isOwner = true;
|
|
||||||
this.createNonAnonymousRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse roles.
|
|
||||||
var member = {};
|
|
||||||
member.show = $(pres).find('>show').text();
|
|
||||||
member.status = $(pres).find('>status').text();
|
|
||||||
var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
|
|
||||||
member.affiliation = tmp.attr('affiliation');
|
|
||||||
member.role = tmp.attr('role');
|
|
||||||
|
|
||||||
// Focus recognition
|
|
||||||
member.jid = tmp.attr('jid');
|
|
||||||
member.isFocus = false;
|
|
||||||
if (member.jid
|
|
||||||
&& member.jid.indexOf(Moderator.getFocusUserJid() + "/") == 0) {
|
|
||||||
member.isFocus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
|
|
||||||
member.displayName = (nicktag.length > 0 ? nicktag.text() : null);
|
|
||||||
|
|
||||||
if (from == this.myroomjid) {
|
|
||||||
if (member.affiliation == 'owner') this.isOwner = true;
|
|
||||||
if (this.role !== member.role) {
|
|
||||||
this.role = member.role;
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED,
|
|
||||||
from, member, pres, Moderator.isModerator());
|
|
||||||
}
|
|
||||||
if (!this.joined) {
|
|
||||||
this.joined = true;
|
|
||||||
console.log("(TIME) MUC joined:\t",
|
|
||||||
window.performance.now());
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
|
|
||||||
this.list_members.push(from);
|
|
||||||
}
|
|
||||||
} else if (this.members[from] === undefined) {
|
|
||||||
// new participant
|
|
||||||
this.members[from] = member;
|
|
||||||
this.list_members.push(from);
|
|
||||||
console.log('entered', from, member);
|
|
||||||
if (member.isFocus) {
|
|
||||||
this.focusMucJid = from;
|
|
||||||
console.info("Ignore focus: " + from + ", real JID: " + member.jid);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var id = $(pres).find('>userId').text();
|
|
||||||
var email = $(pres).find('>email');
|
|
||||||
if (email.length > 0) {
|
|
||||||
id = email.text();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, id, member.displayName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Presence update for existing participant
|
|
||||||
// Watch role change:
|
|
||||||
if (this.members[from].role != member.role) {
|
|
||||||
this.members[from].role = member.role;
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
|
|
||||||
member.role, member.displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the new
|
|
||||||
if(member.displayName)
|
|
||||||
this.members[from].displayName = member.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always trigger presence to update bindings
|
|
||||||
this.parsePresence(from, member, pres);
|
|
||||||
|
|
||||||
// Trigger status message update
|
|
||||||
if (member.status) {
|
|
||||||
eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onPresenceUnavailable: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
// room destroyed ?
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
|
|
||||||
'>destroy').length) {
|
|
||||||
var reason;
|
|
||||||
var reasonSelect = $(pres).find(
|
|
||||||
'>x[xmlns="http://jabber.org/protocol/muc#user"]' +
|
|
||||||
'>destroy>reason');
|
|
||||||
if (reasonSelect.length) {
|
|
||||||
reason = reasonSelect.text();
|
|
||||||
}
|
|
||||||
XMPP.disposeConference(false);
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status code 110 indicates that this notification is "self-presence".
|
|
||||||
if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
|
|
||||||
delete this.members[from];
|
|
||||||
this.list_members.splice(this.list_members.indexOf(from), 1);
|
|
||||||
this.onParticipantLeft(from);
|
|
||||||
}
|
|
||||||
// If the status code is 110 this means we're leaving and we would like
|
|
||||||
// to remove everyone else from our view, so we trigger the event.
|
|
||||||
else if (this.list_members.length > 1) {
|
|
||||||
for (var i = 0; i < this.list_members.length; i++) {
|
|
||||||
var member = this.list_members[i];
|
|
||||||
delete this.members[i];
|
|
||||||
this.list_members.splice(i, 1);
|
|
||||||
this.onParticipantLeft(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
|
|
||||||
$(document).trigger('kicked.muc', [from]);
|
|
||||||
if (this.myroomjid === from) {
|
|
||||||
XMPP.disposeConference(false);
|
|
||||||
eventEmitter.emit(XMPPEvents.KICKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastPresenceMap[from] != null) {
|
|
||||||
delete this.lastPresenceMap[from];
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onPresenceError: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
|
|
||||||
console.log('on password required', from);
|
|
||||||
var self = this;
|
|
||||||
eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED, function (value) {
|
|
||||||
self.doJoin(from, value);
|
|
||||||
});
|
|
||||||
} else if ($(pres).find(
|
|
||||||
'>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
|
|
||||||
var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
|
|
||||||
if (toDomain === config.hosts.anonymousdomain) {
|
|
||||||
// enter the room by replying with 'not-authorized'. This would
|
|
||||||
// result in reconnection from authorized domain.
|
|
||||||
// We're either missing Jicofo/Prosody config for anonymous
|
|
||||||
// domains or something is wrong.
|
|
||||||
// XMPP.promptLogin();
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.warn('onPresError ', pres);
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('onPresError ', pres);
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
sendMessage: function (body, nickname) {
|
|
||||||
var msg = $msg({to: this.roomjid, type: 'groupchat'});
|
|
||||||
msg.c('body', body).up();
|
|
||||||
if (nickname) {
|
|
||||||
msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
|
|
||||||
}
|
|
||||||
this.connection.send(msg);
|
|
||||||
eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
|
|
||||||
},
|
|
||||||
setSubject: function (subject) {
|
|
||||||
var msg = $msg({to: this.roomjid, type: 'groupchat'});
|
|
||||||
msg.c('subject', subject);
|
|
||||||
this.connection.send(msg);
|
|
||||||
console.log("topic changed to " + subject);
|
|
||||||
},
|
|
||||||
onMessage: function (msg) {
|
|
||||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
|
||||||
var from = msg.getAttribute('from');
|
|
||||||
var nick =
|
|
||||||
$(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
|
|
||||||
.text() ||
|
|
||||||
Strophe.getResourceFromJid(from);
|
|
||||||
|
|
||||||
var txt = $(msg).find('>body').text();
|
|
||||||
var type = msg.getAttribute("type");
|
|
||||||
if (type == "error") {
|
|
||||||
eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
|
|
||||||
$(msg).find('>text').text(), txt);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subject = $(msg).find('>subject');
|
|
||||||
if (subject.length) {
|
|
||||||
var subjectText = subject.text();
|
|
||||||
if (subjectText || subjectText == "") {
|
|
||||||
eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
|
|
||||||
console.log("Subject is changed to " + subjectText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// xep-0203 delay
|
|
||||||
var stamp = $(msg).find('>delay').attr('stamp');
|
|
||||||
|
|
||||||
if (!stamp) {
|
|
||||||
// or xep-0091 delay, UTC timestamp
|
|
||||||
stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
|
|
||||||
|
|
||||||
if (stamp) {
|
|
||||||
// the format is CCYYMMDDThh:mm:ss
|
|
||||||
var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
|
|
||||||
stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (txt) {
|
|
||||||
console.log('chat', nick, txt);
|
|
||||||
eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
|
|
||||||
from, nick, txt, this.myroomjid, stamp);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
lockRoom: function (key, onSuccess, onError, onNotSupported) {
|
|
||||||
//http://xmpp.org/extensions/xep-0045.html#roomconfig
|
|
||||||
var ob = this;
|
|
||||||
this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
|
|
||||||
function (res) {
|
|
||||||
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
|
|
||||||
var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
|
|
||||||
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
|
|
||||||
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
|
|
||||||
// Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
|
|
||||||
formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
|
|
||||||
// FIXME: is muc#roomconfig_passwordprotectedroom required?
|
|
||||||
ob.connection.sendIQ(formsubmit,
|
|
||||||
onSuccess,
|
|
||||||
onError);
|
|
||||||
} else {
|
|
||||||
onNotSupported();
|
|
||||||
}
|
|
||||||
}, onError);
|
|
||||||
},
|
|
||||||
kick: function (jid) {
|
|
||||||
var kickIQ = $iq({to: this.roomjid, type: 'set'})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
|
|
||||||
.c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
|
|
||||||
.c('reason').t('You have been kicked.').up().up().up();
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
kickIQ,
|
|
||||||
function (result) {
|
|
||||||
console.log('Kick participant with jid: ', jid, result);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('Kick participant error: ', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sendPresence: function () {
|
|
||||||
if (!this.presMap['to']) {
|
|
||||||
// Too early to send presence - not initialized
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var pres = $pres({to: this.presMap['to'] });
|
|
||||||
pres.c('x', {xmlns: this.presMap['xns']});
|
|
||||||
|
|
||||||
if (this.presMap['password']) {
|
|
||||||
pres.c('password').t(this.presMap['password']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.up();
|
|
||||||
|
|
||||||
// Send XEP-0115 'c' stanza that contains our capabilities info
|
|
||||||
if (this.connection.caps) {
|
|
||||||
this.connection.caps.node = config.clientNode;
|
|
||||||
pres.c('c', this.connection.caps.generateCapsAttrs()).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
|
|
||||||
.t(navigator.userAgent).up();
|
|
||||||
|
|
||||||
if (this.presMap['bridgeIsDown']) {
|
|
||||||
pres.c('bridgeIsDown').up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['email']) {
|
|
||||||
pres.c('email').t(this.presMap['email']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['userId']) {
|
|
||||||
pres.c('userId').t(this.presMap['userId']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['displayName']) {
|
|
||||||
// XEP-0172
|
|
||||||
pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
|
|
||||||
.t(this.presMap['displayName']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.presMap["devices"])
|
|
||||||
{
|
|
||||||
pres.c('devices').c('audio').t(this.presMap['devices'].audio).up()
|
|
||||||
.c('video').t(this.presMap['devices'].video).up().up();
|
|
||||||
}
|
|
||||||
if (this.presMap['audions']) {
|
|
||||||
pres.c('audiomuted', {xmlns: this.presMap['audions']})
|
|
||||||
.t(this.presMap['audiomuted']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['videons']) {
|
|
||||||
pres.c('videomuted', {xmlns: this.presMap['videons']})
|
|
||||||
.t(this.presMap['videomuted']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['videoTypeNs']) {
|
|
||||||
pres.c('videoType', { xmlns: this.presMap['videoTypeNs'] })
|
|
||||||
.t(this.presMap['videoType']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['statsns']) {
|
|
||||||
var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
|
|
||||||
for (var stat in this.presMap["stats"])
|
|
||||||
if (this.presMap["stats"][stat] != null)
|
|
||||||
stats.c("stat", {name: stat, value: this.presMap["stats"][stat]}).up();
|
|
||||||
pres.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['prezins']) {
|
|
||||||
pres.c('prezi',
|
|
||||||
{xmlns: this.presMap['prezins'],
|
|
||||||
'url': this.presMap['preziurl']})
|
|
||||||
.c('current').t(this.presMap['prezicurrent']).up().up();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
if (this.presMap['medians']) {
|
|
||||||
pres.c('media', {xmlns: this.presMap['medians']});
|
|
||||||
var sourceNumber = 0;
|
|
||||||
Object.keys(this.presMap).forEach(function (key) {
|
|
||||||
if (key.indexOf('source') >= 0) {
|
|
||||||
sourceNumber++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (sourceNumber > 0) {
|
|
||||||
for (var i = 1; i <= sourceNumber / 3; i++) {
|
|
||||||
pres.c('source',
|
|
||||||
{
|
|
||||||
type: this.presMap['source' + i + '_type'],
|
|
||||||
ssrc: this.presMap['source' + i + '_ssrc'],
|
|
||||||
direction: this.presMap['source' + i + '_direction']
|
|
||||||
|| 'sendrecv'
|
|
||||||
}
|
|
||||||
).up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pres.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.presMap["startMuted"] !== undefined)
|
|
||||||
{
|
|
||||||
pres.c("startmuted", {audio: this.presMap["startMuted"].audio,
|
|
||||||
video: this.presMap["startMuted"].video,
|
|
||||||
xmlns: "http://jitsi.org/jitmeet/start-muted"});
|
|
||||||
delete this.presMap["startMuted"];
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.up();
|
|
||||||
this.connection.send(pres);
|
|
||||||
},
|
|
||||||
addDisplayNameToPresence: function (displayName) {
|
|
||||||
this.presMap['displayName'] = displayName;
|
|
||||||
},
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
|
|
||||||
if (!this.presMap['medians'])
|
|
||||||
this.presMap['medians'] = 'http://estos.de/ns/mjs';
|
|
||||||
|
|
||||||
this.presMap['source' + sourceNumber + '_type'] = mtype;
|
|
||||||
this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
|
|
||||||
this.presMap['source' + sourceNumber + '_direction'] = direction;
|
|
||||||
},
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
clearPresenceMedia: function () {
|
|
||||||
var self = this;
|
|
||||||
Object.keys(this.presMap).forEach(function (key) {
|
|
||||||
if (key.indexOf('source') != -1) {
|
|
||||||
delete self.presMap[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addDevicesToPresence: function (devices) {
|
|
||||||
this.presMap['devices'] = devices;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Adds the info about the type of our video stream.
|
|
||||||
* @param videoType 'camera' or 'screen'
|
|
||||||
*/
|
|
||||||
addVideoTypeToPresence: function (videoType) {
|
|
||||||
this.presMap['videoTypeNs'] = 'http://jitsi.org/jitmeet/video';
|
|
||||||
this.presMap['videoType'] = videoType;
|
|
||||||
},
|
|
||||||
addPreziToPresence: function (url, currentSlide) {
|
|
||||||
this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
|
|
||||||
this.presMap['preziurl'] = url;
|
|
||||||
this.presMap['prezicurrent'] = currentSlide;
|
|
||||||
},
|
|
||||||
removePreziFromPresence: function () {
|
|
||||||
delete this.presMap['prezins'];
|
|
||||||
delete this.presMap['preziurl'];
|
|
||||||
delete this.presMap['prezicurrent'];
|
|
||||||
},
|
|
||||||
addCurrentSlideToPresence: function (currentSlide) {
|
|
||||||
this.presMap['prezicurrent'] = currentSlide;
|
|
||||||
},
|
|
||||||
getPrezi: function (roomjid) {
|
|
||||||
return this.preziMap[roomjid];
|
|
||||||
},
|
|
||||||
addAudioInfoToPresence: function (isMuted) {
|
|
||||||
this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
|
|
||||||
this.presMap['audiomuted'] = isMuted.toString();
|
|
||||||
},
|
|
||||||
addVideoInfoToPresence: function (isMuted) {
|
|
||||||
this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
|
|
||||||
this.presMap['videomuted'] = isMuted.toString();
|
|
||||||
},
|
|
||||||
addConnectionInfoToPresence: function (stats) {
|
|
||||||
this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
|
|
||||||
this.presMap['stats'] = stats;
|
|
||||||
},
|
|
||||||
findJidFromResource: function (resourceJid) {
|
|
||||||
if (resourceJid &&
|
|
||||||
resourceJid === Strophe.getResourceFromJid(this.myroomjid)) {
|
|
||||||
return this.myroomjid;
|
|
||||||
}
|
|
||||||
var peerJid = null;
|
|
||||||
Object.keys(this.members).some(function (jid) {
|
|
||||||
peerJid = jid;
|
|
||||||
return Strophe.getResourceFromJid(jid) === resourceJid;
|
|
||||||
});
|
|
||||||
return peerJid;
|
|
||||||
},
|
|
||||||
addBridgeIsDownToPresence: function () {
|
|
||||||
this.presMap['bridgeIsDown'] = true;
|
|
||||||
},
|
|
||||||
addEmailToPresence: function (email) {
|
|
||||||
this.presMap['email'] = email;
|
|
||||||
},
|
|
||||||
addUserIdToPresence: function (userId) {
|
|
||||||
this.presMap['userId'] = userId;
|
|
||||||
},
|
|
||||||
addStartMutedToPresence: function (audio, video) {
|
|
||||||
this.presMap["startMuted"] = {audio: audio, video: video};
|
|
||||||
},
|
|
||||||
isModerator: function () {
|
|
||||||
return this.role === 'moderator';
|
|
||||||
},
|
|
||||||
getMemberRole: function (peerJid) {
|
|
||||||
if (this.members[peerJid]) {
|
|
||||||
return this.members[peerJid].role;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onParticipantLeft: function (jid) {
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
|
|
||||||
|
|
||||||
this.connection.jingle.terminateByJid(jid);
|
|
||||||
|
|
||||||
if (this.getPrezi(jid)) {
|
|
||||||
$(document).trigger('presentationremoved.muc',
|
|
||||||
[jid, this.getPrezi(jid)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Moderator.onMucMemberLeft(jid);
|
|
||||||
},
|
|
||||||
parsePresence: function (from, member, pres) {
|
|
||||||
if($(pres).find(">bridgeIsDown").length > 0 && !this.bridgeIsDown) {
|
|
||||||
this.bridgeIsDown = true;
|
|
||||||
eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(member.isFocus)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var displayName = !config.displayJids
|
|
||||||
? member.displayName : Strophe.getResourceFromJid(from);
|
|
||||||
|
|
||||||
if (displayName && displayName.length > 0) {
|
|
||||||
eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = $(pres).find('>userID').text();
|
|
||||||
var email = $(pres).find('>email');
|
|
||||||
if (email.length > 0) {
|
|
||||||
id = email.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,341 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
/* jshint -W101 */
|
|
||||||
|
|
||||||
var JingleSession = require("./JingleSessionPC");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = function(XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('jingle', {
|
|
||||||
connection: null,
|
|
||||||
sessions: {},
|
|
||||||
jid2session: {},
|
|
||||||
ice_config: {iceServers: []},
|
|
||||||
pc_constraints: {},
|
|
||||||
activecall: null,
|
|
||||||
media_constraints: {
|
|
||||||
mandatory: {
|
|
||||||
'OfferToReceiveAudio': true,
|
|
||||||
'OfferToReceiveVideo': true
|
|
||||||
}
|
|
||||||
// MozDontOfferDataChannel: true when this is firefox
|
|
||||||
},
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
if (this.connection.disco) {
|
|
||||||
// http://xmpp.org/extensions/xep-0167.html#support
|
|
||||||
// http://xmpp.org/extensions/xep-0176.html#support
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:1');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:dtls:0');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
|
|
||||||
|
|
||||||
if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera() ||
|
|
||||||
RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
this.connection.disco.addFeature('urn:ietf:rfc:4588');
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is dealt with by SDP O/A so we don't need to announce this
|
|
||||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
|
|
||||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
|
|
||||||
|
|
||||||
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
|
|
||||||
this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
|
|
||||||
|
|
||||||
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
|
|
||||||
}
|
|
||||||
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
|
|
||||||
},
|
|
||||||
onJingle: function (iq) {
|
|
||||||
var sid = $(iq).find('jingle').attr('sid');
|
|
||||||
var action = $(iq).find('jingle').attr('action');
|
|
||||||
var fromJid = iq.getAttribute('from');
|
|
||||||
// send ack first
|
|
||||||
var ack = $iq({type: 'result',
|
|
||||||
to: fromJid,
|
|
||||||
id: iq.getAttribute('id')
|
|
||||||
});
|
|
||||||
console.log('on jingle ' + action + ' from ' + fromJid, iq);
|
|
||||||
var sess = this.sessions[sid];
|
|
||||||
if ('session-initiate' != action) {
|
|
||||||
if (sess === null) {
|
|
||||||
ack.type = 'error';
|
|
||||||
ack.c('error', {type: 'cancel'})
|
|
||||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
|
||||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
|
||||||
this.connection.send(ack);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// local jid is not checked
|
|
||||||
if (fromJid != sess.peerjid) {
|
|
||||||
console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
|
|
||||||
ack.type = 'error';
|
|
||||||
ack.c('error', {type: 'cancel'})
|
|
||||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
|
||||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
|
||||||
this.connection.send(ack);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (sess !== undefined) {
|
|
||||||
// existing session with same session id
|
|
||||||
// this might be out-of-order if the sess.peerjid is the same as from
|
|
||||||
ack.type = 'error';
|
|
||||||
ack.c('error', {type: 'cancel'})
|
|
||||||
.c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
|
|
||||||
console.warn('duplicate session id', sid);
|
|
||||||
this.connection.send(ack);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// FIXME: check for a defined action
|
|
||||||
this.connection.send(ack);
|
|
||||||
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
|
|
||||||
switch (action) {
|
|
||||||
case 'session-initiate':
|
|
||||||
console.log("(TIME) received session-initiate:\t",
|
|
||||||
window.performance.now(), iq);
|
|
||||||
var startMuted = $(iq).find('jingle>startmuted');
|
|
||||||
if (startMuted && startMuted.length > 0) {
|
|
||||||
var audioMuted = startMuted.attr("audio");
|
|
||||||
var videoMuted = startMuted.attr("video");
|
|
||||||
eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
|
|
||||||
audioMuted === "true", videoMuted === "true");
|
|
||||||
}
|
|
||||||
sess = new JingleSession(
|
|
||||||
$(iq).attr('to'), $(iq).find('jingle').attr('sid'),
|
|
||||||
this.connection, XMPP, eventEmitter);
|
|
||||||
// configure session
|
|
||||||
|
|
||||||
sess.media_constraints = this.media_constraints;
|
|
||||||
sess.pc_constraints = this.pc_constraints;
|
|
||||||
sess.ice_config = this.ice_config;
|
|
||||||
|
|
||||||
sess.initialize(fromJid, false);
|
|
||||||
// FIXME: setRemoteDescription should only be done when this call is to be accepted
|
|
||||||
sess.setOffer($(iq).find('>jingle'));
|
|
||||||
|
|
||||||
this.sessions[sess.sid] = sess;
|
|
||||||
this.jid2session[sess.peerjid] = sess;
|
|
||||||
|
|
||||||
// the callback should either
|
|
||||||
// .sendAnswer and .accept
|
|
||||||
// or .sendTerminate -- not necessarily synchronous
|
|
||||||
|
|
||||||
// TODO: do we check activecall == null?
|
|
||||||
this.connection.jingle.activecall = sess;
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
|
|
||||||
|
|
||||||
// TODO: check affiliation and/or role
|
|
||||||
console.log('emuc data for', sess.peerjid,
|
|
||||||
this.connection.emuc.members[sess.peerjid]);
|
|
||||||
sess.sendAnswer();
|
|
||||||
sess.accept();
|
|
||||||
break;
|
|
||||||
case 'session-accept':
|
|
||||||
sess.setAnswer($(iq).find('>jingle'));
|
|
||||||
sess.accept();
|
|
||||||
$(document).trigger('callaccepted.jingle', [sess.sid]);
|
|
||||||
break;
|
|
||||||
case 'session-terminate':
|
|
||||||
if (!sess) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log('terminating...', sess.sid);
|
|
||||||
sess.terminate();
|
|
||||||
this.terminate(sess.sid);
|
|
||||||
if ($(iq).find('>jingle>reason').length) {
|
|
||||||
$(document).trigger('callterminated.jingle', [
|
|
||||||
sess.sid,
|
|
||||||
sess.peerjid,
|
|
||||||
$(iq).find('>jingle>reason>:first')[0].tagName,
|
|
||||||
$(iq).find('>jingle>reason>text').text()
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$(document).trigger('callterminated.jingle',
|
|
||||||
[sess.sid, sess.peerjid]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'transport-info':
|
|
||||||
sess.addIceCandidate($(iq).find('>jingle>content'));
|
|
||||||
break;
|
|
||||||
case 'session-info':
|
|
||||||
var affected;
|
|
||||||
if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
||||||
$(document).trigger('ringing.jingle', [sess.sid]);
|
|
||||||
} else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
||||||
affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
|
||||||
$(document).trigger('mute.jingle', [sess.sid, affected]);
|
|
||||||
} else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
|
||||||
affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
|
||||||
$(document).trigger('unmute.jingle', [sess.sid, affected]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'addsource': // FIXME: proprietary, un-jingleish
|
|
||||||
case 'source-add': // FIXME: proprietary
|
|
||||||
console.info("source-add", iq);
|
|
||||||
sess.addSource($(iq).find('>jingle>content'));
|
|
||||||
break;
|
|
||||||
case 'removesource': // FIXME: proprietary, un-jingleish
|
|
||||||
case 'source-remove': // FIXME: proprietary
|
|
||||||
console.info("source-remove", iq);
|
|
||||||
sess.removeSource($(iq).find('>jingle>content'));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn('jingle action not implemented', action);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
|
|
||||||
var sess = new JingleSession(myjid || this.connection.jid,
|
|
||||||
Math.random().toString(36).substr(2, 12), // random string
|
|
||||||
this.connection, XMPP, eventEmitter);
|
|
||||||
// configure session
|
|
||||||
|
|
||||||
sess.media_constraints = this.media_constraints;
|
|
||||||
sess.pc_constraints = this.pc_constraints;
|
|
||||||
sess.ice_config = this.ice_config;
|
|
||||||
|
|
||||||
sess.initialize(peerjid, true);
|
|
||||||
this.sessions[sess.sid] = sess;
|
|
||||||
this.jid2session[sess.peerjid] = sess;
|
|
||||||
sess.sendOffer();
|
|
||||||
return sess;
|
|
||||||
},
|
|
||||||
terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
|
|
||||||
if (sid === null || sid === undefined) {
|
|
||||||
for (sid in this.sessions) {
|
|
||||||
if (this.sessions[sid].state != 'ended') {
|
|
||||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
|
||||||
this.sessions[sid].terminate();
|
|
||||||
}
|
|
||||||
delete this.jid2session[this.sessions[sid].peerjid];
|
|
||||||
delete this.sessions[sid];
|
|
||||||
}
|
|
||||||
} else if (this.sessions.hasOwnProperty(sid)) {
|
|
||||||
if (this.sessions[sid].state != 'ended') {
|
|
||||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
|
||||||
this.sessions[sid].terminate();
|
|
||||||
}
|
|
||||||
delete this.jid2session[this.sessions[sid].peerjid];
|
|
||||||
delete this.sessions[sid];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Used to terminate a session when an unavailable presence is received.
|
|
||||||
terminateByJid: function (jid) {
|
|
||||||
if (this.jid2session.hasOwnProperty(jid)) {
|
|
||||||
var sess = this.jid2session[jid];
|
|
||||||
if (sess) {
|
|
||||||
sess.terminate();
|
|
||||||
console.log('peer went away silently', jid);
|
|
||||||
delete this.sessions[sess.sid];
|
|
||||||
delete this.jid2session[jid];
|
|
||||||
$(document).trigger('callterminated.jingle',
|
|
||||||
[sess.sid, jid], 'gone');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
terminateRemoteByJid: function (jid, reason) {
|
|
||||||
if (this.jid2session.hasOwnProperty(jid)) {
|
|
||||||
var sess = this.jid2session[jid];
|
|
||||||
if (sess) {
|
|
||||||
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
|
|
||||||
sess.terminate();
|
|
||||||
console.log('terminate peer with jid', sess.sid, jid);
|
|
||||||
delete this.sessions[sess.sid];
|
|
||||||
delete this.jid2session[jid];
|
|
||||||
$(document).trigger('callterminated.jingle',
|
|
||||||
[sess.sid, jid, 'kicked']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getStunAndTurnCredentials: function () {
|
|
||||||
// get stun and turn configuration from server via xep-0215
|
|
||||||
// uses time-limited credentials as described in
|
|
||||||
// http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
|
||||||
//
|
|
||||||
// see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
|
|
||||||
// for a prosody module which implements this
|
|
||||||
//
|
|
||||||
// currently, this doesn't work with updateIce and therefore credentials with a long
|
|
||||||
// validity have to be fetched before creating the peerconnection
|
|
||||||
// TODO: implement refresh via updateIce as described in
|
|
||||||
// https://code.google.com/p/webrtc/issues/detail?id=1650
|
|
||||||
var self = this;
|
|
||||||
this.connection.sendIQ(
|
|
||||||
$iq({type: 'get', to: this.connection.domain})
|
|
||||||
.c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
|
|
||||||
function (res) {
|
|
||||||
var iceservers = [];
|
|
||||||
$(res).find('>services>service').each(function (idx, el) {
|
|
||||||
el = $(el);
|
|
||||||
var dict = {};
|
|
||||||
var type = el.attr('type');
|
|
||||||
switch (type) {
|
|
||||||
case 'stun':
|
|
||||||
dict.url = 'stun:' + el.attr('host');
|
|
||||||
if (el.attr('port')) {
|
|
||||||
dict.url += ':' + el.attr('port');
|
|
||||||
}
|
|
||||||
iceservers.push(dict);
|
|
||||||
break;
|
|
||||||
case 'turn':
|
|
||||||
case 'turns':
|
|
||||||
dict.url = type + ':';
|
|
||||||
if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
|
|
||||||
if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
|
|
||||||
dict.url += el.attr('username') + '@';
|
|
||||||
} else {
|
|
||||||
dict.username = el.attr('username'); // only works in M28
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dict.url += el.attr('host');
|
|
||||||
if (el.attr('port') && el.attr('port') != '3478') {
|
|
||||||
dict.url += ':' + el.attr('port');
|
|
||||||
}
|
|
||||||
if (el.attr('transport') && el.attr('transport') != 'udp') {
|
|
||||||
dict.url += '?transport=' + el.attr('transport');
|
|
||||||
}
|
|
||||||
if (el.attr('password')) {
|
|
||||||
dict.credential = el.attr('password');
|
|
||||||
}
|
|
||||||
iceservers.push(dict);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.ice_config.iceServers = iceservers;
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
console.warn('getting turn credentials failed', err);
|
|
||||||
console.warn('is mod_turncredentials or similar installed?');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// implement push?
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data saved in 'updateLog' in a format to be logged.
|
|
||||||
*/
|
|
||||||
getLog: function () {
|
|
||||||
var data = {};
|
|
||||||
var self = this;
|
|
||||||
Object.keys(this.sessions).forEach(function (sid) {
|
|
||||||
var session = self.sessions[sid];
|
|
||||||
if (session.peerconnection && session.peerconnection.updateLog) {
|
|
||||||
// FIXME: should probably be a .dump call
|
|
||||||
data["jingle_" + session.sid] = {
|
|
||||||
updateLog: session.peerconnection.updateLog,
|
|
||||||
stats: session.peerconnection.stats,
|
|
||||||
url: window.location.href
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/* global Strophe */
|
|
||||||
module.exports = function () {
|
|
||||||
|
|
||||||
Strophe.addConnectionPlugin('logger', {
|
|
||||||
// logs raw stanzas and makes them available for download as JSON
|
|
||||||
connection: null,
|
|
||||||
log: [],
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
this.connection.rawInput = this.log_incoming.bind(this);
|
|
||||||
this.connection.rawOutput = this.log_outgoing.bind(this);
|
|
||||||
},
|
|
||||||
log_incoming: function (stanza) {
|
|
||||||
this.log.push([new Date().getTime(), 'incoming', stanza]);
|
|
||||||
},
|
|
||||||
log_outgoing: function (stanza) {
|
|
||||||
this.log.push([new Date().getTime(), 'outgoing', stanza]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,61 +0,0 @@
|
||||||
/* global $, $iq, config, connection, focusMucJid, forceMuted,
|
|
||||||
setAudioMuted, Strophe */
|
|
||||||
/**
|
|
||||||
* Moderate connection plugin.
|
|
||||||
*/
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
module.exports = function (XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('moderate', {
|
|
||||||
connection: null,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
|
|
||||||
this.connection.addHandler(this.onMute.bind(this),
|
|
||||||
'http://jitsi.org/jitmeet/audio',
|
|
||||||
'iq',
|
|
||||||
'set',
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
},
|
|
||||||
setMute: function (jid, mute) {
|
|
||||||
console.info("set mute", mute);
|
|
||||||
var iqToFocus =
|
|
||||||
$iq({to: this.connection.emuc.focusMucJid, type: 'set'})
|
|
||||||
.c('mute', {
|
|
||||||
xmlns: 'http://jitsi.org/jitmeet/audio',
|
|
||||||
jid: jid
|
|
||||||
})
|
|
||||||
.t(mute.toString())
|
|
||||||
.up();
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
iqToFocus,
|
|
||||||
function (result) {
|
|
||||||
console.log('set mute', result);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('set mute error', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onMute: function (iq) {
|
|
||||||
var from = iq.getAttribute('from');
|
|
||||||
if (from !== this.connection.emuc.focusMucJid) {
|
|
||||||
console.warn("Ignored mute from non focus peer");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var mute = $(iq).find('mute');
|
|
||||||
if (mute.length) {
|
|
||||||
var doMuteAudio = mute.text() === "true";
|
|
||||||
eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, doMuteAudio);
|
|
||||||
XMPP.forceMuted = doMuteAudio;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
eject: function (jid) {
|
|
||||||
// We're not the focus, so can't terminate
|
|
||||||
//connection.jingle.terminateRemoteByJid(jid, 'kick');
|
|
||||||
this.connection.emuc.kick(jid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,121 +0,0 @@
|
||||||
/* global $, $iq, Strophe */
|
|
||||||
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping every 10 sec
|
|
||||||
*/
|
|
||||||
var PING_INTERVAL = 10000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping timeout error after 15 sec of waiting.
|
|
||||||
*/
|
|
||||||
var PING_TIMEOUT = 15000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will close the connection after 3 consecutive ping errors.
|
|
||||||
*/
|
|
||||||
var PING_THRESHOLD = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XEP-0199 ping plugin.
|
|
||||||
*
|
|
||||||
* Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
|
|
||||||
*/
|
|
||||||
module.exports = function (XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('ping', {
|
|
||||||
|
|
||||||
connection: null,
|
|
||||||
|
|
||||||
failedPings: 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the plugin. Method called by Strophe.
|
|
||||||
* @param connection Strophe connection instance.
|
|
||||||
*/
|
|
||||||
init: function (connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
Strophe.addNamespace('PING', "urn:xmpp:ping");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends "ping" to given <tt>jid</tt>
|
|
||||||
* @param jid the JID to which ping request will be sent.
|
|
||||||
* @param success callback called on success.
|
|
||||||
* @param error callback called on error.
|
|
||||||
* @param timeout ms how long are we going to wait for the response. On
|
|
||||||
* timeout <tt>error<//t> callback is called with undefined error
|
|
||||||
* argument.
|
|
||||||
*/
|
|
||||||
ping: function (jid, success, error, timeout) {
|
|
||||||
var iq = $iq({type: 'get', to: jid});
|
|
||||||
iq.c('ping', {xmlns: Strophe.NS.PING});
|
|
||||||
this.connection.sendIQ(iq, success, error, timeout);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if given <tt>jid</tt> has XEP-0199 ping support.
|
|
||||||
* @param jid the JID to be checked for ping support.
|
|
||||||
* @param callback function with boolean argument which will be
|
|
||||||
* <tt>true</tt> if XEP-0199 ping is supported by given <tt>jid</tt>
|
|
||||||
*/
|
|
||||||
hasPingSupport: function (jid, callback) {
|
|
||||||
this.connection.disco.info(
|
|
||||||
jid, null,
|
|
||||||
function (result) {
|
|
||||||
var ping = $(result).find('>>feature[var="urn:xmpp:ping"]');
|
|
||||||
callback(ping.length > 0);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Ping feature discovery error", error);
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts to send ping in given interval to specified remote JID.
|
|
||||||
* This plugin supports only one such task and <tt>stopInterval</tt>
|
|
||||||
* must be called before starting a new one.
|
|
||||||
* @param remoteJid remote JID to which ping requests will be sent to.
|
|
||||||
* @param interval task interval in ms.
|
|
||||||
*/
|
|
||||||
startInterval: function (remoteJid, interval) {
|
|
||||||
if (this.intervalId) {
|
|
||||||
console.error("Ping task scheduled already");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!interval)
|
|
||||||
interval = PING_INTERVAL;
|
|
||||||
var self = this;
|
|
||||||
this.intervalId = window.setInterval(function () {
|
|
||||||
self.ping(remoteJid,
|
|
||||||
function (result) {
|
|
||||||
// Ping OK
|
|
||||||
self.failedPings = 0;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.failedPings += 1;
|
|
||||||
console.error(
|
|
||||||
"Ping " + (error ? "error" : "timeout"), error);
|
|
||||||
if (self.failedPings >= PING_THRESHOLD) {
|
|
||||||
self.connection.disconnect();
|
|
||||||
}
|
|
||||||
}, PING_TIMEOUT);
|
|
||||||
}, interval);
|
|
||||||
console.info("XMPP pings will be sent every " + interval + " ms");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops current "ping" interval task.
|
|
||||||
*/
|
|
||||||
stopInterval: function () {
|
|
||||||
if (this.intervalId) {
|
|
||||||
window.clearInterval(this.intervalId);
|
|
||||||
this.intervalId = null;
|
|
||||||
this.failedPings = 0;
|
|
||||||
console.info("Ping interval cleared");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,96 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
module.exports = function() {
|
|
||||||
Strophe.addConnectionPlugin('rayo',
|
|
||||||
{
|
|
||||||
RAYO_XMLNS: 'urn:xmpp:rayo:1',
|
|
||||||
connection: null,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
if (this.connection.disco) {
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connection.addHandler(
|
|
||||||
this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set',
|
|
||||||
null, null);
|
|
||||||
},
|
|
||||||
onRayo: function (iq) {
|
|
||||||
console.info("Rayo IQ", iq);
|
|
||||||
},
|
|
||||||
dial: function (to, from, roomName, roomPass) {
|
|
||||||
var self = this;
|
|
||||||
var req = $iq(
|
|
||||||
{
|
|
||||||
type: 'set',
|
|
||||||
to: this.connection.emuc.focusMucJid
|
|
||||||
}
|
|
||||||
);
|
|
||||||
req.c('dial',
|
|
||||||
{
|
|
||||||
xmlns: this.RAYO_XMLNS,
|
|
||||||
to: to,
|
|
||||||
from: from
|
|
||||||
});
|
|
||||||
req.c('header',
|
|
||||||
{
|
|
||||||
name: 'JvbRoomName',
|
|
||||||
value: roomName
|
|
||||||
}).up();
|
|
||||||
|
|
||||||
if (roomPass !== null && roomPass.length) {
|
|
||||||
|
|
||||||
req.c('header',
|
|
||||||
{
|
|
||||||
name: 'JvbRoomPassword',
|
|
||||||
value: roomPass
|
|
||||||
}).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
req,
|
|
||||||
function (result) {
|
|
||||||
console.info('Dial result ', result);
|
|
||||||
|
|
||||||
var resource = $(result).find('ref').attr('uri');
|
|
||||||
self.call_resource = resource.substr('xmpp:'.length);
|
|
||||||
console.info(
|
|
||||||
"Received call resource: " + self.call_resource);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.info('Dial error ', error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
hang_up: function () {
|
|
||||||
if (!this.call_resource) {
|
|
||||||
console.warn("No call in progress");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
var req = $iq(
|
|
||||||
{
|
|
||||||
type: 'set',
|
|
||||||
to: this.call_resource
|
|
||||||
}
|
|
||||||
);
|
|
||||||
req.c('hangup',
|
|
||||||
{
|
|
||||||
xmlns: this.RAYO_XMLNS
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
req,
|
|
||||||
function (result) {
|
|
||||||
console.info('Hangup result ', result);
|
|
||||||
self.call_resource = null;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.info('Hangup error ', error);
|
|
||||||
self.call_resource = null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,43 +0,0 @@
|
||||||
/* global Strophe */
|
|
||||||
/**
|
|
||||||
* Strophe logger implementation. Logs from level WARN and above.
|
|
||||||
*/
|
|
||||||
module.exports = function () {
|
|
||||||
|
|
||||||
Strophe.log = function (level, msg) {
|
|
||||||
switch (level) {
|
|
||||||
case Strophe.LogLevel.WARN:
|
|
||||||
console.warn("Strophe: " + msg);
|
|
||||||
break;
|
|
||||||
case Strophe.LogLevel.ERROR:
|
|
||||||
case Strophe.LogLevel.FATAL:
|
|
||||||
console.error("Strophe: " + msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Strophe.getStatusString = function (status) {
|
|
||||||
switch (status) {
|
|
||||||
case Strophe.Status.ERROR:
|
|
||||||
return "ERROR";
|
|
||||||
case Strophe.Status.CONNECTING:
|
|
||||||
return "CONNECTING";
|
|
||||||
case Strophe.Status.CONNFAIL:
|
|
||||||
return "CONNFAIL";
|
|
||||||
case Strophe.Status.AUTHENTICATING:
|
|
||||||
return "AUTHENTICATING";
|
|
||||||
case Strophe.Status.AUTHFAIL:
|
|
||||||
return "AUTHFAIL";
|
|
||||||
case Strophe.Status.CONNECTED:
|
|
||||||
return "CONNECTED";
|
|
||||||
case Strophe.Status.DISCONNECTED:
|
|
||||||
return "DISCONNECTED";
|
|
||||||
case Strophe.Status.DISCONNECTING:
|
|
||||||
return "DISCONNECTING";
|
|
||||||
case Strophe.Status.ATTACHED:
|
|
||||||
return "ATTACHED";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,624 +0,0 @@
|
||||||
/* global $, APP, config, Strophe, Base64, $msg */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
var EventEmitter = require("events");
|
|
||||||
var Recording = require("./recording");
|
|
||||||
var SDP = require("./SDP");
|
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
var Settings = require("../settings/Settings");
|
|
||||||
var Pako = require("pako");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var retry = require('retry');
|
|
||||||
var RandomUtil = require("../util/RandomUtil");
|
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
|
||||||
var connection = null;
|
|
||||||
var authenticatedUser = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method that generates user name based on random hex values.
|
|
||||||
* Eg. 12345678-1234-1234-12345678
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function generateUserName() {
|
|
||||||
return RandomUtil.randomHexString(8) + "-" + RandomUtil.randomHexString(4)
|
|
||||||
+ "-" + RandomUtil.randomHexString(4) + "-"
|
|
||||||
+ RandomUtil.randomHexString(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect(jid, password) {
|
|
||||||
|
|
||||||
var faultTolerantConnect = retry.operation({
|
|
||||||
retries: 3
|
|
||||||
});
|
|
||||||
|
|
||||||
// fault tolerant connect
|
|
||||||
faultTolerantConnect.attempt(function () {
|
|
||||||
|
|
||||||
connection = XMPP.createConnection();
|
|
||||||
Moderator.setConnection(connection);
|
|
||||||
|
|
||||||
connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
|
|
||||||
if (config.useIPv6) {
|
|
||||||
// https://code.google.com/p/webrtc/issues/detail?id=2828
|
|
||||||
if (!connection.jingle.pc_constraints.optional)
|
|
||||||
connection.jingle.pc_constraints.optional = [];
|
|
||||||
connection.jingle.pc_constraints.optional.push({googIPv6: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include user info in MUC presence
|
|
||||||
var settings = Settings.getSettings();
|
|
||||||
if (settings.email) {
|
|
||||||
connection.emuc.addEmailToPresence(settings.email);
|
|
||||||
}
|
|
||||||
if (settings.uid) {
|
|
||||||
connection.emuc.addUserIdToPresence(settings.uid);
|
|
||||||
}
|
|
||||||
if (settings.displayName) {
|
|
||||||
connection.emuc.addDisplayNameToPresence(settings.displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// connection.connect() starts the connection process.
|
|
||||||
//
|
|
||||||
// As the connection process proceeds, the user supplied callback will
|
|
||||||
// be triggered multiple times with status updates. The callback should
|
|
||||||
// take two arguments - the status code and the error condition.
|
|
||||||
//
|
|
||||||
// The status code will be one of the values in the Strophe.Status
|
|
||||||
// constants. The error condition will be one of the conditions defined
|
|
||||||
// in RFC 3920 or the condition ‘strophe-parsererror’.
|
|
||||||
//
|
|
||||||
// The Parameters wait, hold and route are optional and only relevant
|
|
||||||
// for BOSH connections. Please see XEP 124 for a more detailed
|
|
||||||
// explanation of the optional parameters.
|
|
||||||
//
|
|
||||||
// Connection status constants for use by the connection handler
|
|
||||||
// callback.
|
|
||||||
//
|
|
||||||
// Status.ERROR - An error has occurred (websockets specific)
|
|
||||||
// Status.CONNECTING - The connection is currently being made
|
|
||||||
// Status.CONNFAIL - The connection attempt failed
|
|
||||||
// Status.AUTHENTICATING - The connection is authenticating
|
|
||||||
// Status.AUTHFAIL - The authentication attempt failed
|
|
||||||
// Status.CONNECTED - The connection has succeeded
|
|
||||||
// Status.DISCONNECTED - The connection has been terminated
|
|
||||||
// Status.DISCONNECTING - The connection is currently being terminated
|
|
||||||
// Status.ATTACHED - The connection has been attached
|
|
||||||
|
|
||||||
var anonymousConnectionFailed = false;
|
|
||||||
var connectionFailed = false;
|
|
||||||
var lastErrorMsg;
|
|
||||||
connection.connect(jid, password, function (status, msg) {
|
|
||||||
console.log("(TIME) Strophe " + Strophe.getStatusString(status) +
|
|
||||||
(msg ? "[" + msg + "]" : "") +
|
|
||||||
"\t:" + window.performance.now());
|
|
||||||
if (status === Strophe.Status.CONNECTED) {
|
|
||||||
if (config.useStunTurn) {
|
|
||||||
connection.jingle.getStunAndTurnCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("My Jabber ID: " + connection.jid);
|
|
||||||
|
|
||||||
// Schedule ping ?
|
|
||||||
var pingJid = connection.domain;
|
|
||||||
connection.ping.hasPingSupport(
|
|
||||||
pingJid,
|
|
||||||
function (hasPing) {
|
|
||||||
if (hasPing)
|
|
||||||
connection.ping.startInterval(pingJid);
|
|
||||||
else
|
|
||||||
console.warn("Ping NOT supported by " + pingJid);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (password)
|
|
||||||
authenticatedUser = true;
|
|
||||||
maybeDoJoin();
|
|
||||||
} else if (status === Strophe.Status.CONNFAIL) {
|
|
||||||
if (msg === 'x-strophe-bad-non-anon-jid') {
|
|
||||||
anonymousConnectionFailed = true;
|
|
||||||
} else {
|
|
||||||
connectionFailed = true;
|
|
||||||
}
|
|
||||||
lastErrorMsg = msg;
|
|
||||||
} else if (status === Strophe.Status.DISCONNECTED) {
|
|
||||||
// Stop ping interval
|
|
||||||
connection.ping.stopInterval();
|
|
||||||
if (anonymousConnectionFailed) {
|
|
||||||
// prompt user for username and password
|
|
||||||
XMPP.promptLogin();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Strophe already has built-in HTTP/BOSH error handling and
|
|
||||||
// request retry logic. Requests are resent automatically
|
|
||||||
// until their error count reaches 5. Strophe.js disconnects
|
|
||||||
// if the error count is > 5. We are not replicating this
|
|
||||||
// here.
|
|
||||||
//
|
|
||||||
// The "problem" is that failed HTTP/BOSH requests don't
|
|
||||||
// trigger a callback with a status update, so when a
|
|
||||||
// callback with status Strophe.Status.DISCONNECTED arrives,
|
|
||||||
// we can't be sure if it's a graceful disconnect or if it's
|
|
||||||
// triggered by some HTTP/BOSH error.
|
|
||||||
//
|
|
||||||
// But that's a minor issue in Jitsi Meet as we never
|
|
||||||
// disconnect anyway, not even when the user closes the
|
|
||||||
// browser window (which is kind of wrong, but the point is
|
|
||||||
// that we should never ever get disconnected).
|
|
||||||
//
|
|
||||||
// On the other hand, failed connections due to XMPP layer
|
|
||||||
// errors, trigger a callback with status Strophe.Status.CONNFAIL.
|
|
||||||
//
|
|
||||||
// Here we implement retry logic for failed connections due
|
|
||||||
// to XMPP layer errors and we display an error to the user
|
|
||||||
// if we get disconnected from the XMPP server permanently.
|
|
||||||
|
|
||||||
// If the connection failed, retry.
|
|
||||||
if (connectionFailed &&
|
|
||||||
faultTolerantConnect.retry("connection-failed")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we failed to connect to the XMPP server, fire an event
|
|
||||||
// to let all the interested module now about it.
|
|
||||||
eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
|
|
||||||
msg ? msg : lastErrorMsg);
|
|
||||||
}
|
|
||||||
} else if (status === Strophe.Status.AUTHFAIL) {
|
|
||||||
// wrong password or username, prompt user
|
|
||||||
XMPP.promptLogin();
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function maybeDoJoin() {
|
|
||||||
if (connection && connection.connected &&
|
|
||||||
Strophe.getResourceFromJid(connection.jid) &&
|
|
||||||
(APP.RTC.localAudio || APP.RTC.localVideo)) {
|
|
||||||
// .connected is true while connecting?
|
|
||||||
doJoin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doJoin() {
|
|
||||||
eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStrophePlugins()
|
|
||||||
{
|
|
||||||
require("./strophe.emuc")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.jingle")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.moderate")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.util")();
|
|
||||||
require("./strophe.ping")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.rayo")();
|
|
||||||
require("./strophe.logger")();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If given <tt>localStream</tt> is video one this method will advertise it's
|
|
||||||
* video type in MUC presence.
|
|
||||||
* @param localStream new or modified <tt>LocalStream</tt>.
|
|
||||||
*/
|
|
||||||
function broadcastLocalVideoType(localStream) {
|
|
||||||
if (localStream.videoType)
|
|
||||||
XMPP.addToPresence('videoType', localStream.videoType);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerListeners() {
|
|
||||||
APP.RTC.addStreamListener(
|
|
||||||
function (localStream) {
|
|
||||||
maybeDoJoin();
|
|
||||||
broadcastLocalVideoType(localStream);
|
|
||||||
},
|
|
||||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
|
|
||||||
);
|
|
||||||
APP.RTC.addStreamListener(
|
|
||||||
broadcastLocalVideoType,
|
|
||||||
StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
|
|
||||||
);
|
|
||||||
APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
|
|
||||||
XMPP.addToPresence("devices", devices);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var unload = (function () {
|
|
||||||
var unloaded = false;
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (unloaded) { return; }
|
|
||||||
unloaded = true;
|
|
||||||
|
|
||||||
if (connection && connection.connected) {
|
|
||||||
// ensure signout
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: config.bosh,
|
|
||||||
async: false,
|
|
||||||
cache: false,
|
|
||||||
contentType: 'application/xml',
|
|
||||||
data: "<body rid='" +
|
|
||||||
(connection.rid || connection._proto.rid) +
|
|
||||||
"' xmlns='http://jabber.org/protocol/httpbind' sid='" +
|
|
||||||
(connection.sid || connection._proto.sid) +
|
|
||||||
"' type='terminate'>" +
|
|
||||||
"<presence xmlns='jabber:client' type='unavailable'/>" +
|
|
||||||
"</body>",
|
|
||||||
success: function (data) {
|
|
||||||
console.log('signed out');
|
|
||||||
console.log(data);
|
|
||||||
},
|
|
||||||
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
|
||||||
console.log('signout error',
|
|
||||||
textStatus + ' (' + errorThrown + ')');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
XMPP.disposeConference(true);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
function setupEvents() {
|
|
||||||
// In recent versions of FF the 'beforeunload' event is not fired when the
|
|
||||||
// window or the tab is closed. It is only fired when we leave the page
|
|
||||||
// (change URL). If this participant doesn't unload properly, then it
|
|
||||||
// becomes a ghost for the rest of the participants that stay in the
|
|
||||||
// conference. Thankfully handling the 'unload' event in addition to the
|
|
||||||
// 'beforeunload' event seems to guarantee the execution of the 'unload'
|
|
||||||
// method at least once.
|
|
||||||
//
|
|
||||||
// The 'unload' method can safely be run multiple times, it will actually do
|
|
||||||
// something only the first time that it's run, so we're don't have to worry
|
|
||||||
// about browsers that fire both events.
|
|
||||||
|
|
||||||
$(window).bind('beforeunload', unload);
|
|
||||||
$(window).bind('unload', unload);
|
|
||||||
}
|
|
||||||
|
|
||||||
var XMPP = {
|
|
||||||
getConnection: function(){ return connection; },
|
|
||||||
sessionTerminated: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XMPP connection status
|
|
||||||
*/
|
|
||||||
Status: Strophe.Status,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remembers if we were muted by the focus.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
forceMuted: false,
|
|
||||||
start: function () {
|
|
||||||
setupEvents();
|
|
||||||
initStrophePlugins();
|
|
||||||
registerListeners();
|
|
||||||
Moderator.init(this, eventEmitter);
|
|
||||||
Recording.init();
|
|
||||||
var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
|
|
||||||
// Force authenticated domain if room is appended with '?login=true'
|
|
||||||
if (config.hosts.anonymousdomain &&
|
|
||||||
window.location.search.indexOf("login=true") !== -1) {
|
|
||||||
configDomain = config.hosts.domain;
|
|
||||||
}
|
|
||||||
var jid = configDomain || window.location.hostname;
|
|
||||||
connect(jid);
|
|
||||||
},
|
|
||||||
createConnection: function () {
|
|
||||||
var bosh = config.bosh || '/http-bind';
|
|
||||||
// adds the room name used to the bosh connection
|
|
||||||
bosh += '?room=' + APP.UI.getRoomNode();
|
|
||||||
if (config.token) {
|
|
||||||
bosh += "&token=" + config.token;
|
|
||||||
}
|
|
||||||
return new Strophe.Connection(bosh);
|
|
||||||
},
|
|
||||||
getStatusString: function (status) {
|
|
||||||
return Strophe.getStatusString(status);
|
|
||||||
},
|
|
||||||
promptLogin: function () {
|
|
||||||
eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN, connect);
|
|
||||||
},
|
|
||||||
joinRoom: function(roomName, useNicks, nick) {
|
|
||||||
var roomjid = roomName;
|
|
||||||
|
|
||||||
if (useNicks) {
|
|
||||||
if (nick) {
|
|
||||||
roomjid += '/' + nick;
|
|
||||||
} else {
|
|
||||||
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var tmpJid = Strophe.getNodeFromJid(connection.jid);
|
|
||||||
|
|
||||||
if(!authenticatedUser)
|
|
||||||
tmpJid = tmpJid.substr(0, 8);
|
|
||||||
|
|
||||||
roomjid += '/' + tmpJid;
|
|
||||||
}
|
|
||||||
connection.emuc.doJoin(roomjid);
|
|
||||||
},
|
|
||||||
myJid: function () {
|
|
||||||
if(!connection)
|
|
||||||
return null;
|
|
||||||
return connection.emuc.myroomjid;
|
|
||||||
},
|
|
||||||
myResource: function () {
|
|
||||||
if(!connection || ! connection.emuc.myroomjid)
|
|
||||||
return null;
|
|
||||||
return Strophe.getResourceFromJid(connection.emuc.myroomjid);
|
|
||||||
},
|
|
||||||
getLastPresence: function (from) {
|
|
||||||
if(!connection)
|
|
||||||
return null;
|
|
||||||
return connection.emuc.lastPresenceMap[from];
|
|
||||||
},
|
|
||||||
disposeConference: function (onUnload) {
|
|
||||||
var handler = connection.jingle.activecall;
|
|
||||||
if (handler && handler.peerconnection) {
|
|
||||||
// FIXME: probably removing streams is not required and close() should
|
|
||||||
// be enough
|
|
||||||
if (APP.RTC.localAudio) {
|
|
||||||
handler.peerconnection.removeStream(
|
|
||||||
APP.RTC.localAudio.getOriginalStream(), onUnload);
|
|
||||||
}
|
|
||||||
if (APP.RTC.localVideo) {
|
|
||||||
handler.peerconnection.removeStream(
|
|
||||||
APP.RTC.localVideo.getOriginalStream(), onUnload);
|
|
||||||
}
|
|
||||||
handler.peerconnection.close();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
|
|
||||||
connection.jingle.activecall = null;
|
|
||||||
if (!onUnload) {
|
|
||||||
this.sessionTerminated = true;
|
|
||||||
connection.emuc.doLeave();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addListener: function(type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
},
|
|
||||||
removeListener: function (type, listener) {
|
|
||||||
eventEmitter.removeListener(type, listener);
|
|
||||||
},
|
|
||||||
allocateConferenceFocus: function(roomName, callback) {
|
|
||||||
Moderator.allocateConferenceFocus(roomName, callback);
|
|
||||||
},
|
|
||||||
getLoginUrl: function (roomName, callback) {
|
|
||||||
Moderator.getLoginUrl(roomName, callback);
|
|
||||||
},
|
|
||||||
getPopupLoginUrl: function (roomName, callback) {
|
|
||||||
Moderator.getPopupLoginUrl(roomName, callback);
|
|
||||||
},
|
|
||||||
isModerator: function () {
|
|
||||||
return Moderator.isModerator();
|
|
||||||
},
|
|
||||||
isSipGatewayEnabled: function () {
|
|
||||||
return Moderator.isSipGatewayEnabled();
|
|
||||||
},
|
|
||||||
isExternalAuthEnabled: function () {
|
|
||||||
return Moderator.isExternalAuthEnabled();
|
|
||||||
},
|
|
||||||
isConferenceInProgress: function () {
|
|
||||||
return connection && connection.jingle.activecall &&
|
|
||||||
connection.jingle.activecall.peerconnection;
|
|
||||||
},
|
|
||||||
switchStreams: function (stream, oldStream, callback, isAudio) {
|
|
||||||
if (this.isConferenceInProgress()) {
|
|
||||||
// FIXME: will block switchInProgress on true value in case of exception
|
|
||||||
connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
|
|
||||||
} else {
|
|
||||||
// We are done immediately
|
|
||||||
console.warn("No conference handler or conference not started yet");
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendVideoInfoPresence: function (mute) {
|
|
||||||
if(!connection)
|
|
||||||
return;
|
|
||||||
connection.emuc.addVideoInfoToPresence(mute);
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
setVideoMute: function (mute, callback, options) {
|
|
||||||
if(!connection)
|
|
||||||
return;
|
|
||||||
var self = this;
|
|
||||||
var localCallback = function (mute) {
|
|
||||||
self.sendVideoInfoPresence(mute);
|
|
||||||
return callback(mute);
|
|
||||||
};
|
|
||||||
|
|
||||||
if(connection.jingle.activecall)
|
|
||||||
{
|
|
||||||
connection.jingle.activecall.setVideoMute(
|
|
||||||
mute, localCallback, options);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
localCallback(mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
setAudioMute: function (mute, callback) {
|
|
||||||
if (!(connection && APP.RTC.localAudio)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.forceMuted && !mute) {
|
|
||||||
console.info("Asking focus for unmute");
|
|
||||||
connection.moderate.setMute(connection.emuc.myroomjid, mute);
|
|
||||||
// FIXME: wait for result before resetting muted status
|
|
||||||
this.forceMuted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mute == APP.RTC.localAudio.isMuted()) {
|
|
||||||
// Nothing to do
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
APP.RTC.localAudio.setMute(mute);
|
|
||||||
this.sendAudioInfoPresence(mute, callback);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
sendAudioInfoPresence: function(mute, callback) {
|
|
||||||
if(connection) {
|
|
||||||
connection.emuc.addAudioInfoToPresence(mute);
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
toggleRecording: function (tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback) {
|
|
||||||
Recording.toggleRecording(tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback, connection);
|
|
||||||
},
|
|
||||||
addToPresence: function (name, value, dontSend) {
|
|
||||||
switch (name) {
|
|
||||||
case "displayName":
|
|
||||||
connection.emuc.addDisplayNameToPresence(value);
|
|
||||||
break;
|
|
||||||
case "prezi":
|
|
||||||
connection.emuc.addPreziToPresence(value, 0);
|
|
||||||
break;
|
|
||||||
case "preziSlide":
|
|
||||||
connection.emuc.addCurrentSlideToPresence(value);
|
|
||||||
break;
|
|
||||||
case "connectionQuality":
|
|
||||||
connection.emuc.addConnectionInfoToPresence(value);
|
|
||||||
break;
|
|
||||||
case "email":
|
|
||||||
connection.emuc.addEmailToPresence(value);
|
|
||||||
break;
|
|
||||||
case "devices":
|
|
||||||
connection.emuc.addDevicesToPresence(value);
|
|
||||||
break;
|
|
||||||
case "videoType":
|
|
||||||
connection.emuc.addVideoTypeToPresence(value);
|
|
||||||
break;
|
|
||||||
case "startMuted":
|
|
||||||
if(!Moderator.isModerator())
|
|
||||||
return;
|
|
||||||
connection.emuc.addStartMutedToPresence(value[0],
|
|
||||||
value[1]);
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
console.log("Unknown tag for presence: " + name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dontSend)
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Sends 'data' as a log message to the focus. Returns true iff a message
|
|
||||||
* was sent.
|
|
||||||
* @param data
|
|
||||||
* @returns {boolean} true iff a message was sent.
|
|
||||||
*/
|
|
||||||
sendLogs: function (data) {
|
|
||||||
if(!connection.emuc.focusMucJid)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var deflate = true;
|
|
||||||
|
|
||||||
var content = JSON.stringify(data);
|
|
||||||
if (deflate) {
|
|
||||||
content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
|
|
||||||
}
|
|
||||||
content = Base64.encode(content);
|
|
||||||
// XEP-0337-ish
|
|
||||||
var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
|
|
||||||
message.c('log', { xmlns: 'urn:xmpp:eventlog',
|
|
||||||
id: 'PeerConnectionStats'});
|
|
||||||
message.c('message').t(content).up();
|
|
||||||
if (deflate) {
|
|
||||||
message.c('tag', {name: "deflated", value: "true"}).up();
|
|
||||||
}
|
|
||||||
message.up();
|
|
||||||
|
|
||||||
connection.send(message);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
// Gets the logs from strophe.jingle.
|
|
||||||
getJingleLog: function () {
|
|
||||||
return connection.jingle ? connection.jingle.getLog() : {};
|
|
||||||
},
|
|
||||||
// Gets the logs from strophe.
|
|
||||||
getXmppLog: function () {
|
|
||||||
return connection.logger ? connection.logger.log : null;
|
|
||||||
},
|
|
||||||
getPrezi: function () {
|
|
||||||
return connection.emuc.getPrezi(this.myJid());
|
|
||||||
},
|
|
||||||
removePreziFromPresence: function () {
|
|
||||||
connection.emuc.removePreziFromPresence();
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
sendChatMessage: function (message, nickname) {
|
|
||||||
connection.emuc.sendMessage(message, nickname);
|
|
||||||
},
|
|
||||||
setSubject: function (topic) {
|
|
||||||
connection.emuc.setSubject(topic);
|
|
||||||
},
|
|
||||||
lockRoom: function (key, onSuccess, onError, onNotSupported) {
|
|
||||||
connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
|
|
||||||
},
|
|
||||||
dial: function (to, from, roomName,roomPass) {
|
|
||||||
connection.rayo.dial(to, from, roomName,roomPass);
|
|
||||||
},
|
|
||||||
setMute: function (jid, mute) {
|
|
||||||
connection.moderate.setMute(jid, mute);
|
|
||||||
},
|
|
||||||
eject: function (jid) {
|
|
||||||
connection.moderate.eject(jid);
|
|
||||||
},
|
|
||||||
logout: function (callback) {
|
|
||||||
Moderator.logout(callback);
|
|
||||||
},
|
|
||||||
findJidFromResource: function (resource) {
|
|
||||||
return connection.emuc.findJidFromResource(resource);
|
|
||||||
},
|
|
||||||
getMembers: function () {
|
|
||||||
return connection.emuc.members;
|
|
||||||
},
|
|
||||||
getJidFromSSRC: function (ssrc) {
|
|
||||||
if (!this.isConferenceInProgress())
|
|
||||||
return null;
|
|
||||||
return connection.jingle.activecall.getSsrcOwner(ssrc);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Gets the SSRC of local media stream.
|
|
||||||
* @param mediaType the media type that tells whether we want to get
|
|
||||||
* the SSRC of local audio or video stream.
|
|
||||||
* @returns {*} the SSRC number for local media stream or <tt>null</tt> if
|
|
||||||
* not available.
|
|
||||||
*/
|
|
||||||
getLocalSSRC: function (mediaType) {
|
|
||||||
if (!this.isConferenceInProgress()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return connection.jingle.activecall.getLocalSSRC(mediaType);
|
|
||||||
},
|
|
||||||
// Returns true iff we have joined the MUC.
|
|
||||||
isMUCJoined: function () {
|
|
||||||
return connection === null ? false : connection.emuc.joined;
|
|
||||||
},
|
|
||||||
getSessions: function () {
|
|
||||||
return connection.jingle.sessions;
|
|
||||||
},
|
|
||||||
removeStream: function (stream) {
|
|
||||||
if (!this.isConferenceInProgress())
|
|
||||||
return;
|
|
||||||
connection.jingle.activecall.peerconnection.removeStream(stream);
|
|
||||||
},
|
|
||||||
filter_special_chars: function (text) {
|
|
||||||
return SDPUtil.filter_special_chars(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = XMPP;
|
|
13
package.json
13
package.json
|
@ -42,7 +42,10 @@
|
||||||
"jshint": "2.8.0",
|
"jshint": "2.8.0",
|
||||||
"precommit-hook": "3.0.0",
|
"precommit-hook": "3.0.0",
|
||||||
"uglify-js": "2.4.24",
|
"uglify-js": "2.4.24",
|
||||||
"clean-css": "*"
|
"clean-css": "*",
|
||||||
|
"babelify": "*",
|
||||||
|
"babel-preset-es2015": "*",
|
||||||
|
"babel-polyfill": "*"
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -54,9 +57,15 @@
|
||||||
],
|
],
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [
|
"transform": [
|
||||||
"browserify-shim"
|
"browserify-shim",
|
||||||
|
["babelify", {
|
||||||
|
"ignore": "node_modules"
|
||||||
|
}]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": ["es2015"]
|
||||||
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"jquery": "./node_modules/jquery/dist/jquery.js",
|
"jquery": "./node_modules/jquery/dist/jquery.js",
|
||||||
"jquery-ui": "./node_modules/jquery-ui/jquery-ui.js",
|
"jquery-ui": "./node_modules/jquery-ui/jquery-ui.js",
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
--- /usr/lib/prosody/modules/mod_bosh.lua 2015-12-16 14:28:34.000000000 -0600
|
||||||
|
+++ /usr/lib/prosody/modules/mod_bosh.lua 2015-12-22 10:45:59.818197967 -0600
|
||||||
|
@@ -294,6 +294,9 @@
|
||||||
|
|
||||||
|
session.log("debug", "BOSH session created for request from %s", session.ip);
|
||||||
|
log("info", "New BOSH session, assigned it sid '%s'", sid);
|
||||||
|
+
|
||||||
|
+ hosts[session.host].events.fire_event(
|
||||||
|
+ "bosh-session", { session = session, request = request });
|
||||||
|
|
||||||
|
-- Send creation response
|
||||||
|
local creating_session = true;
|
|
@ -1,6 +0,0 @@
|
||||||
var MediaStreamType = {
|
|
||||||
VIDEO_TYPE: "video",
|
|
||||||
|
|
||||||
AUDIO_TYPE: "audio"
|
|
||||||
};
|
|
||||||
module.exports = MediaStreamType;
|
|
|
@ -1,53 +0,0 @@
|
||||||
var Resolutions = {
|
|
||||||
"1080": {
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
order: 7
|
|
||||||
},
|
|
||||||
"fullhd": {
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
order: 7
|
|
||||||
},
|
|
||||||
"720": {
|
|
||||||
width: 1280,
|
|
||||||
height: 720,
|
|
||||||
order: 6
|
|
||||||
},
|
|
||||||
"hd": {
|
|
||||||
width: 1280,
|
|
||||||
height: 720,
|
|
||||||
order: 6
|
|
||||||
},
|
|
||||||
"960": {
|
|
||||||
width: 960,
|
|
||||||
height: 720,
|
|
||||||
order: 5
|
|
||||||
},
|
|
||||||
"640": {
|
|
||||||
width: 640,
|
|
||||||
height: 480,
|
|
||||||
order: 4
|
|
||||||
},
|
|
||||||
"vga": {
|
|
||||||
width: 640,
|
|
||||||
height: 480,
|
|
||||||
order: 4
|
|
||||||
},
|
|
||||||
"360": {
|
|
||||||
width: 640,
|
|
||||||
height: 360,
|
|
||||||
order: 3
|
|
||||||
},
|
|
||||||
"320": {
|
|
||||||
width: 320,
|
|
||||||
height: 240,
|
|
||||||
order: 2
|
|
||||||
},
|
|
||||||
"180": {
|
|
||||||
width: 320,
|
|
||||||
height: 180,
|
|
||||||
order: 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
module.exports = Resolutions;
|
|
|
@ -1,12 +1,45 @@
|
||||||
var UIEvents = {
|
export default {
|
||||||
NICKNAME_CHANGED: "UI.nickname_changed",
|
NICKNAME_CHANGED: "UI.nickname_changed",
|
||||||
SELECTED_ENDPOINT: "UI.selected_endpoint",
|
SELECTED_ENDPOINT: "UI.selected_endpoint",
|
||||||
PINNED_ENDPOINT: "UI.pinned_endpoint",
|
PINNED_ENDPOINT: "UI.pinned_endpoint",
|
||||||
LARGEVIDEO_INIT: "UI.largevideo_init",
|
|
||||||
/**
|
/**
|
||||||
* Notifies interested parties when the film strip (remote video's panel)
|
* Notifies that local user created text message.
|
||||||
* is hidden (toggled) or shown (un-toggled).
|
|
||||||
*/
|
*/
|
||||||
FILM_STRIP_TOGGLED: "UI.filmstrip_toggled"
|
MESSAGE_CREATED: "UI.message_created",
|
||||||
|
/**
|
||||||
|
* Notifies that local user changed language.
|
||||||
|
*/
|
||||||
|
LANG_CHANGED: "UI.lang_changed",
|
||||||
|
/**
|
||||||
|
* Notifies that local user changed email.
|
||||||
|
*/
|
||||||
|
EMAIL_CHANGED: "UI.email_changed",
|
||||||
|
/**
|
||||||
|
* Notifies that "start muted" settings changed.
|
||||||
|
*/
|
||||||
|
START_MUTED_CHANGED: "UI.start_muted_changed",
|
||||||
|
AUDIO_MUTED: "UI.audio_muted",
|
||||||
|
VIDEO_MUTED: "UI.video_muted",
|
||||||
|
PREZI_CLICKED: "UI.prezi_clicked",
|
||||||
|
SHARE_PREZI: "UI.share_prezi",
|
||||||
|
PREZI_SLIDE_CHANGED: "UI.prezi_slide_changed",
|
||||||
|
STOP_SHARING_PREZI: "UI.stop_sharing_prezi",
|
||||||
|
ETHERPAD_CLICKED: "UI.etherpad_clicked",
|
||||||
|
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
|
||||||
|
USER_INVITED: "UI.user_invited",
|
||||||
|
USER_KICKED: "UI.user_kicked",
|
||||||
|
REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
|
||||||
|
FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
|
||||||
|
AUTH_CLICKED: "UI.auth_clicked",
|
||||||
|
TOGGLE_CHAT: "UI.toggle_chat",
|
||||||
|
TOGGLE_SETTINGS: "UI.toggle_settings",
|
||||||
|
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
|
||||||
|
TOGGLE_FILM_STRIP: "UI.toggle_film_strip",
|
||||||
|
TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
|
||||||
|
CONTACT_CLICKED: "UI.contact_clicked",
|
||||||
|
HANGUP: "UI.hangup",
|
||||||
|
LOGOUT: "UI.logout",
|
||||||
|
RECORDING_TOGGLE: "UI.recording_toggle",
|
||||||
|
SIP_DIAL: "UI.sip_dial",
|
||||||
|
SUBEJCT_CHANGED: "UI.subject_changed"
|
||||||
};
|
};
|
||||||
module.exports = UIEvents;
|
|
|
@ -1,6 +1,4 @@
|
||||||
var DesktopSharingEventTypes = {
|
export default {
|
||||||
INIT: "ds.init",
|
|
||||||
|
|
||||||
SWITCHING_DONE: "ds.switching_done",
|
SWITCHING_DONE: "ds.switching_done",
|
||||||
|
|
||||||
NEW_STREAM_CREATED: "ds.new_stream_created",
|
NEW_STREAM_CREATED: "ds.new_stream_created",
|
||||||
|
@ -11,5 +9,3 @@ var DesktopSharingEventTypes = {
|
||||||
*/
|
*/
|
||||||
FIREFOX_EXTENSION_NEEDED: "ds.firefox_extension_needed"
|
FIREFOX_EXTENSION_NEEDED: "ds.firefox_extension_needed"
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = DesktopSharingEventTypes;
|
|
|
@ -1,5 +0,0 @@
|
||||||
var Events = {
|
|
||||||
DTMF_SUPPORT_CHANGED: "members.dtmf_support_changed"
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Events;
|
|
|
@ -86,8 +86,6 @@ var XMPPEvents = {
|
||||||
JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error',
|
JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error',
|
||||||
PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
|
PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
|
||||||
FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
|
FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
|
||||||
ROOM_JOIN_ERROR: 'xmpp.room_join_error',
|
|
||||||
ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
|
|
||||||
// xmpp is connected and obtained user media
|
// xmpp is connected and obtained user media
|
||||||
READY_TO_JOIN: 'xmpp.ready_to_join'
|
READY_TO_JOIN: 'xmpp.ready_to_join'
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue