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
|
||||
lib-jitsi-meet.js -text -diff
|
||||
|
|
|
@ -2,7 +2,10 @@ node_modules
|
|||
libs
|
||||
debian
|
||||
analytics.js
|
||||
lib-jitsi-meet.js
|
||||
|
||||
modules/xmpp/strophe.emuc.js
|
||||
modules/UI/prezi/Prezi.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()`
|
||||
"maxlen": 80, // {int} Max number of characters per line
|
||||
"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
|
||||
OUTPUT_DIR = .
|
||||
|
||||
all: compile uglify deploy clean
|
||||
all: update-deps compile uglify deploy clean
|
||||
|
||||
update-deps:
|
||||
$(NPM) update
|
||||
|
||||
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:
|
||||
rm -f $(OUTPUT_DIR)/app.bundle.*
|
||||
|
|
122
app.js
122
app.js
|
@ -1,48 +1,99 @@
|
|||
/* jshint -W117 */
|
||||
/* global $, JitsiMeetJS, config */
|
||||
/* application specific logic */
|
||||
|
||||
require("jquery");
|
||||
require("jquery-ui");
|
||||
require("strophe");
|
||||
require("strophe-disco");
|
||||
require("strophe-caps");
|
||||
require("tooltip");
|
||||
require("popover");
|
||||
import "babel-polyfill";
|
||||
import "jquery";
|
||||
import "jquery-ui";
|
||||
import "strophe";
|
||||
import "strophe-disco";
|
||||
import "strophe-caps";
|
||||
import "tooltip";
|
||||
import "popover";
|
||||
import "jQuery-Impromptu";
|
||||
import "autosize";
|
||||
window.toastr = require("toastr");
|
||||
require("jQuery-Impromptu");
|
||||
require("autosize");
|
||||
|
||||
var APP =
|
||||
{
|
||||
init: function () {
|
||||
this.UI = require("./modules/UI/UI");
|
||||
this.API = require("./modules/API/API");
|
||||
import URLProcessor from "./modules/config/URLProcessor";
|
||||
import RoomnameGenerator from './modules/util/RoomnameGenerator';
|
||||
|
||||
import UI from "./modules/UI/UI";
|
||||
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 =
|
||||
require("./modules/connectionquality/connectionquality");
|
||||
this.statistics = require("./modules/statistics/statistics");
|
||||
this.RTC = require("./modules/RTC/RTC");
|
||||
this.desktopsharing =
|
||||
require("./modules/desktopsharing/desktopsharing");
|
||||
this.xmpp = require("./modules/xmpp/xmpp");
|
||||
this.keyboardshortcut =
|
||||
require("./modules/keyboardshortcut/keyboardshortcut");
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
function init() {
|
||||
var isUIReady = APP.UI.start();
|
||||
if (isUIReady) {
|
||||
APP.conference.init({roomName: buildRoomName()}).then(function () {
|
||||
APP.UI.initConference();
|
||||
|
||||
APP.desktopsharing.init();
|
||||
APP.RTC.start();
|
||||
APP.xmpp.start();
|
||||
APP.statistics.start();
|
||||
APP.connectionquality.init();
|
||||
APP.keyboardshortcut.init();
|
||||
APP.members.start();
|
||||
APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {
|
||||
APP.translation.setLanguage(language);
|
||||
APP.settings.setLanguage(language);
|
||||
});
|
||||
|
||||
APP.desktopsharing.init(JitsiMeetJS.isDesktopSharingEnabled());
|
||||
APP.statistics.start();
|
||||
APP.connectionquality.init();
|
||||
APP.keyboardshortcut.init();
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,7 +105,7 @@ function init() {
|
|||
* will be displayed to the user.
|
||||
*/
|
||||
function obtainConfigAndInit() {
|
||||
var roomName = APP.UI.getRoomNode();
|
||||
let roomName = APP.conference.roomName;
|
||||
|
||||
if (config.configLocation) {
|
||||
APP.configFetch.obtainConfig(
|
||||
|
@ -84,23 +135,18 @@ function obtainConfigAndInit() {
|
|||
$(document).ready(function () {
|
||||
console.log("(TIME) document ready:\t", window.performance.now());
|
||||
|
||||
var URLProcessor = require("./modules/config/URLProcessor");
|
||||
URLProcessor.setConfigParametersFromUrl();
|
||||
APP.init();
|
||||
|
||||
APP.translation.init();
|
||||
APP.translation.init(settings.getLanguage());
|
||||
|
||||
if(APP.API.isEnabled())
|
||||
APP.API.init();
|
||||
|
||||
APP.UI.start(obtainConfigAndInit);
|
||||
APP.API.init();
|
||||
|
||||
obtainConfigAndInit();
|
||||
});
|
||||
|
||||
$(window).bind('beforeunload', function () {
|
||||
if(APP.API.isEnabled())
|
||||
APP.API.dispose();
|
||||
APP.API.dispose();
|
||||
});
|
||||
|
||||
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 {
|
||||
display: inline-block;
|
||||
display: none;
|
||||
background-color: black;
|
||||
background-size: contain;
|
||||
border-radius:8px;
|
||||
|
@ -378,7 +378,7 @@
|
|||
padding-right:2px;
|
||||
height:38px;
|
||||
width:auto;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
border: 1px solid rgba(256, 256, 256, 0.2);
|
||||
border-radius: 6px;
|
||||
pointer-events: auto;
|
||||
|
@ -407,7 +407,7 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
#activeSpeaker {
|
||||
#dominantSpeaker {
|
||||
visibility: hidden;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
|
@ -416,7 +416,7 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
#activeSpeakerAudioLevel {
|
||||
#dominantSpeakerAudioLevel {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
@ -428,7 +428,7 @@
|
|||
display:none !important;
|
||||
}
|
||||
|
||||
#activeSpeakerAvatar {
|
||||
#dominantSpeakerAvatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 25px;
|
||||
|
|
|
@ -35,6 +35,6 @@ Description: Prosody configuration for Jitsi Meet
|
|||
|
||||
Package: jitsi-meet-tokens
|
||||
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
|
||||
|
||||
|
|
|
@ -74,6 +74,11 @@ case "$1" in
|
|||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
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
|
||||
echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet"
|
||||
fi
|
||||
|
|
24
index.html
24
index.html
|
@ -13,6 +13,7 @@
|
|||
<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="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>
|
||||
<!--
|
||||
Link used for inline installation of chrome desktop streaming extension,
|
||||
|
@ -139,13 +140,32 @@
|
|||
</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="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">
|
||||
<span id="localVideoContainer" class="videocontainer">
|
||||
<span id="localNick" class="nick"></span>
|
||||
<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>
|
||||
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
||||
<audio id="localAudio" autoplay muted></audio>
|
||||
<span class="focusindicator"></span>
|
||||
</span>
|
||||
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
||||
|
|
|
@ -14,7 +14,7 @@ var interfaceConfig = {
|
|||
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
||||
APP_NAME: "Jitsi Meet",
|
||||
INVITATION_POWERED_BY: true,
|
||||
ACTIVE_SPEAKER_AVATAR_SIZE: 100,
|
||||
DOMINANT_SPEAKER_AVATAR_SIZE: 100,
|
||||
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
|
||||
'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad',
|
||||
'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
|
||||
*/
|
||||
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
/**
|
||||
* List of the available commands.
|
||||
* @type {{
|
||||
|
@ -23,8 +21,8 @@ var commands = {};
|
|||
function initCommands() {
|
||||
commands = {
|
||||
displayName: APP.UI.inputDisplayNameHandler,
|
||||
toggleAudio: APP.UI.toggleAudio,
|
||||
toggleVideo: APP.UI.toggleVideo,
|
||||
toggleAudio: APP.conference.toggleAudioMuted,
|
||||
toggleVideo: APP.conference.toggleVideoMuted,
|
||||
toggleFilmStrip: APP.UI.toggleFilmStrip,
|
||||
toggleChat: APP.UI.toggleChat,
|
||||
toggleContactList: APP.UI.toggleContactList
|
||||
|
@ -43,7 +41,7 @@ function initCommands() {
|
|||
* participantLeft: boolean
|
||||
* }}
|
||||
*/
|
||||
var events = {
|
||||
const events = {
|
||||
incomingMessage: false,
|
||||
outgoingMessage:false,
|
||||
displayNameChange: false,
|
||||
|
@ -51,8 +49,6 @@ var events = {
|
|||
participantLeft: false
|
||||
};
|
||||
|
||||
var displayName = {};
|
||||
|
||||
/**
|
||||
* Processes commands from external application.
|
||||
* @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});
|
||||
});
|
||||
/**
|
||||
* Check whether the API should be enabled or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEnabled () {
|
||||
let hash = location.hash;
|
||||
return hash && hash.indexOf("external") > -1 && window.postMessage;
|
||||
}
|
||||
|
||||
var API = {
|
||||
/**
|
||||
* Check whether the API should be enabled or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEnabled: function () {
|
||||
var hash = location.hash;
|
||||
if (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
|
||||
* receive information from external applications that embed Jitsi Meet.
|
||||
|
@ -173,50 +167,85 @@ var API = {
|
|||
* is initialized.
|
||||
*/
|
||||
init: function () {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
initCommands();
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message',
|
||||
processMessage, false);
|
||||
}
|
||||
else {
|
||||
window.addEventListener('message', processMessage, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', processMessage);
|
||||
}
|
||||
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
|
||||
* for that event.
|
||||
* @param name the name event
|
||||
* @param object data associated with the event
|
||||
* Notify external application (if API is enabled) that message was sent.
|
||||
* @param {string} body message body
|
||||
*/
|
||||
triggerEvent: function (name, object) {
|
||||
if(this.isEnabled() && this.isEventEnabled(name))
|
||||
sendMessage({
|
||||
type: "event", action: "result", event: name, result: object});
|
||||
notifySendingChatMessage (body) {
|
||||
triggerEvent("outgoingMessage", {"message": body});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
dispose: function () {
|
||||
if(window.removeEventListener) {
|
||||
window.removeEventListener("message",
|
||||
processMessage, false);
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
|
||||
if (window.removeEventListener) {
|
||||
window.removeEventListener("message", processMessage, false);
|
||||
} else {
|
||||
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.
|
||||
*/
|
||||
var messageHandler = require("./util/MessageHandler");
|
||||
var callStats = require("../statistics/CallStats");
|
||||
var APP = require("../../app");
|
||||
|
||||
/**
|
||||
* Constructs the html for the overall feedback window.
|
||||
|
@ -79,7 +77,7 @@ var Feedback = {
|
|||
init: function () {
|
||||
// CallStats is the way we send feedback, so we don't have to initialise
|
||||
// if callstats isn't enabled.
|
||||
if (!this.isEnabled())
|
||||
if (!APP.conference.isCallstatsEnabled())
|
||||
return;
|
||||
|
||||
$("div.feedbackButton").css("display", "block");
|
||||
|
@ -93,10 +91,7 @@ var Feedback = {
|
|||
* @return true if the feedback functionality is enabled, false otherwise.
|
||||
*/
|
||||
isEnabled: function() {
|
||||
// XXX callStats.isEnabled() indicates whether we are allowed to attempt
|
||||
// to integrate callstats.io. Whether our attempt was/is/will be
|
||||
// successful is a different question.
|
||||
return callStats.isEnabled();
|
||||
return APP.conference.isCallstatsEnabled();
|
||||
},
|
||||
/**
|
||||
* Opens the feedback window.
|
||||
|
@ -122,7 +117,7 @@ var Feedback = {
|
|||
// If the feedback is less than 3 stars we're going to
|
||||
// ask the user for more information.
|
||||
if (Feedback.feedbackScore > 3) {
|
||||
callStats.sendFeedback(Feedback.feedbackScore, "");
|
||||
APP.conference.sendFeedback(Feedback.feedbackScore, "");
|
||||
if (feedbackWindowCallback)
|
||||
feedbackWindowCallback();
|
||||
else
|
||||
|
@ -164,7 +159,7 @@ var Feedback = {
|
|||
= document.getElementById("feedbackTextArea").value;
|
||||
|
||||
if (feedbackDetails && feedbackDetails.length > 0)
|
||||
callStats.sendFeedback( Feedback.feedbackScore,
|
||||
APP.conference.sendFeedback( Feedback.feedbackScore,
|
||||
feedbackDetails);
|
||||
|
||||
if (feedbackWindowCallback)
|
||||
|
|
1449
modules/UI/UI.js
1449
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 */
|
||||
var CanvasUtil = require("./CanvasUtils");
|
||||
|
||||
var ASDrawContext = null;
|
||||
import CanvasUtil from './CanvasUtils';
|
||||
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||
|
||||
function initActiveSpeakerAudioLevels() {
|
||||
var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
|
||||
var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
|
||||
const LOCAL_LEVEL = 'local';
|
||||
|
||||
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.
|
||||
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
|
||||
|
@ -17,250 +22,214 @@ function initActiveSpeakerAudioLevels() {
|
|||
ASDrawContext.shadowOffsetY = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the given audio level canvas to match the given thumbnail size.
|
||||
*/
|
||||
function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
|
||||
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
|
||||
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the audio level canvas into the cached canvas object.
|
||||
*
|
||||
* @param id of the user for whom we draw the audio level
|
||||
* @param audioLevel the newAudio level to render
|
||||
*/
|
||||
function drawAudioLevelCanvas(id, audioLevel) {
|
||||
if (!audioLevelCanvasCache[id]) {
|
||||
|
||||
let videoSpanId = getVideoSpanId(id);
|
||||
|
||||
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
|
||||
|
||||
/*
|
||||
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
|
||||
* In such a case, the method CanvasUtil.cloneCanvas may throw an
|
||||
* error. Since audio levels are frequently updated, the errors have
|
||||
* been observed to pile into the console, strain the CPU.
|
||||
*/
|
||||
if (audioLevelCanvasOrig) {
|
||||
audioLevelCanvasCache[id] = CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = audioLevelCanvasCache[id];
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let drawContext = canvas.getContext('2d');
|
||||
|
||||
drawContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
let shadowLevel = getShadowLevel(audioLevel);
|
||||
|
||||
if (shadowLevel > 0) {
|
||||
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
|
||||
CanvasUtil.drawRoundRectGlow(drawContext,
|
||||
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
|
||||
canvas.width - interfaceConfig.CANVAS_EXTRA,
|
||||
canvas.height - interfaceConfig.CANVAS_EXTRA,
|
||||
interfaceConfig.CANVAS_RADIUS,
|
||||
interfaceConfig.SHADOW_COLOR,
|
||||
shadowLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shadow/glow level for the given audio level.
|
||||
*
|
||||
* @param audioLevel the audio level from which we determine the shadow
|
||||
* level
|
||||
*/
|
||||
function getShadowLevel (audioLevel) {
|
||||
let shadowLevel = 0;
|
||||
|
||||
if (audioLevel <= 0.3) {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
|
||||
} else if (audioLevel <= 0.6) {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
|
||||
} else {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
|
||||
}
|
||||
|
||||
return shadowLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the video span id corresponding to the given user id
|
||||
*/
|
||||
function getVideoSpanId(id) {
|
||||
let videoSpanId = null;
|
||||
|
||||
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
|
||||
videoSpanId = 'localVideoContainer';
|
||||
} else {
|
||||
videoSpanId = `participant_${id}`;
|
||||
}
|
||||
|
||||
return videoSpanId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio Levels plugin.
|
||||
*/
|
||||
var AudioLevels = (function(my) {
|
||||
var audioLevelCanvasCache = {};
|
||||
const AudioLevels = {
|
||||
|
||||
my.LOCAL_LEVEL = 'local';
|
||||
|
||||
my.init = function () {
|
||||
ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d');
|
||||
initActiveSpeakerAudioLevels();
|
||||
};
|
||||
init () {
|
||||
ASDrawContext = $('#dominantSpeakerAudioLevel')[0].getContext('2d');
|
||||
initDominantSpeakerAudioLevels();
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the audio level canvas for the given peerJid. If the canvas
|
||||
* Updates the audio level canvas for the given id. 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;
|
||||
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
|
||||
let videoSpanId = 'localVideoContainer';
|
||||
if (id) {
|
||||
videoSpanId = `participant_${id}`;
|
||||
}
|
||||
|
||||
var videoSpan = document.getElementById(videoSpanId);
|
||||
let videoSpan = document.getElementById(videoSpanId);
|
||||
|
||||
if (!videoSpan) {
|
||||
if (resourceJid)
|
||||
console.error("No video element for jid", resourceJid);
|
||||
else
|
||||
if (id) {
|
||||
console.error("No video element for id", id);
|
||||
} 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];
|
||||
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,
|
||||
thumbnailWidth,
|
||||
thumbnailHeight);
|
||||
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,
|
||||
thumbnailWidth,
|
||||
thumbnailHeight);
|
||||
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the audio level UI for the given resourceJid.
|
||||
* Updates the audio level UI for the given id.
|
||||
*
|
||||
* @param resourceJid the resource jid indicating the video element for
|
||||
* which we draw the audio level
|
||||
* @param id id of the user for whom we draw the audio level
|
||||
* @param audioLevel the newAudio level to render
|
||||
*/
|
||||
my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) {
|
||||
drawAudioLevelCanvas(resourceJid, audioLevel);
|
||||
updateAudioLevel (id, audioLevel, largeVideoId) {
|
||||
drawAudioLevelCanvas(id, audioLevel);
|
||||
|
||||
var videoSpanId = getVideoSpanId(resourceJid);
|
||||
let videoSpanId = getVideoSpanId(id);
|
||||
|
||||
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
|
||||
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
|
||||
|
||||
if (!audioLevelCanvas)
|
||||
if (!audioLevelCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
var drawContext = audioLevelCanvas.getContext('2d');
|
||||
let drawContext = audioLevelCanvas.getContext('2d');
|
||||
|
||||
var canvasCache = audioLevelCanvasCache[resourceJid];
|
||||
let canvasCache = audioLevelCanvasCache[id];
|
||||
|
||||
drawContext.clearRect (0, 0,
|
||||
audioLevelCanvas.width, audioLevelCanvas.height);
|
||||
drawContext.clearRect(
|
||||
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
|
||||
);
|
||||
drawContext.drawImage(canvasCache, 0, 0);
|
||||
|
||||
if(resourceJid === AudioLevels.LOCAL_LEVEL) {
|
||||
if(!APP.xmpp.myJid()) {
|
||||
if (id === LOCAL_LEVEL) {
|
||||
id = APP.conference.localId;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
resourceJid = APP.xmpp.myResource();
|
||||
}
|
||||
|
||||
if(resourceJid === largeVideoResourceJid) {
|
||||
if(id === largeVideoId) {
|
||||
window.requestAnimationFrame(function () {
|
||||
AudioLevels.updateActiveSpeakerAudioLevel(audioLevel);
|
||||
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
my.updateActiveSpeakerAudioLevel = function(audioLevel) {
|
||||
if($("#activeSpeaker").css("visibility") == "hidden" || ASDrawContext === null)
|
||||
updateDominantSpeakerAudioLevel (audioLevel) {
|
||||
if($("#domiantSpeaker").css("visibility") == "hidden" || ASDrawContext === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASDrawContext.clearRect(0, 0, 300, 300);
|
||||
if (!audioLevel)
|
||||
if (!audioLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
|
||||
|
||||
|
||||
// Fill the shape.
|
||||
ASDrawContext.fill();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the given audio level canvas to match the given thumbnail size.
|
||||
*/
|
||||
function resizeAudioLevelCanvas(audioLevelCanvas,
|
||||
thumbnailWidth,
|
||||
thumbnailHeight) {
|
||||
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
|
||||
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
|
||||
}
|
||||
updateCanvasSize (thumbWidth, thumbHeight) {
|
||||
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
|
||||
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
|
||||
|
||||
/**
|
||||
* Draws the audio level canvas into the cached canvas object.
|
||||
*
|
||||
* @param resourceJid the resource jid indicating the video element for
|
||||
* which we draw the audio level
|
||||
* @param audioLevel the newAudio level to render
|
||||
*/
|
||||
function drawAudioLevelCanvas(resourceJid, audioLevel) {
|
||||
if (!audioLevelCanvasCache[resourceJid]) {
|
||||
BottomToolbar.getThumbs().children('canvas').width(canvasWidth).height(canvasHeight);
|
||||
|
||||
var videoSpanId = getVideoSpanId(resourceJid);
|
||||
|
||||
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
|
||||
|
||||
/*
|
||||
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
|
||||
* In such a case, the method CanvasUtil.cloneCanvas may throw an
|
||||
* error. Since audio levels are frequently updated, the errors have
|
||||
* been observed to pile into the console, strain the CPU.
|
||||
*/
|
||||
if (audioLevelCanvasOrig) {
|
||||
audioLevelCanvasCache[resourceJid] =
|
||||
CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
|
||||
}
|
||||
}
|
||||
|
||||
var canvas = audioLevelCanvasCache[resourceJid];
|
||||
|
||||
if (!canvas)
|
||||
return;
|
||||
|
||||
var drawContext = canvas.getContext('2d');
|
||||
|
||||
drawContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var shadowLevel = getShadowLevel(audioLevel);
|
||||
|
||||
if (shadowLevel > 0) {
|
||||
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
|
||||
CanvasUtil.drawRoundRectGlow(drawContext,
|
||||
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
|
||||
canvas.width - interfaceConfig.CANVAS_EXTRA,
|
||||
canvas.height - interfaceConfig.CANVAS_EXTRA,
|
||||
interfaceConfig.CANVAS_RADIUS,
|
||||
interfaceConfig.SHADOW_COLOR,
|
||||
shadowLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shadow/glow level for the given audio level.
|
||||
*
|
||||
* @param audioLevel the audio level from which we determine the shadow
|
||||
* level
|
||||
*/
|
||||
function getShadowLevel (audioLevel) {
|
||||
var shadowLevel = 0;
|
||||
|
||||
if (audioLevel <= 0.3) {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
|
||||
}
|
||||
else if (audioLevel <= 0.6) {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
|
||||
}
|
||||
else {
|
||||
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
|
||||
}
|
||||
return shadowLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the video span id corresponding to the given resourceJid or local
|
||||
* user.
|
||||
*/
|
||||
function getVideoSpanId(resourceJid) {
|
||||
var videoSpanId = null;
|
||||
if (resourceJid === AudioLevels.LOCAL_LEVEL ||
|
||||
(APP.xmpp.myResource() && resourceJid === APP.xmpp.myResource()))
|
||||
videoSpanId = 'localVideoContainer';
|
||||
else
|
||||
videoSpanId = 'participant_' + resourceJid;
|
||||
|
||||
return videoSpanId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the remote video has been resized.
|
||||
*/
|
||||
$(document).bind('remotevideo.resized', function (event, width, height) {
|
||||
var resized = false;
|
||||
$('#remoteVideos>span>canvas').each(function() {
|
||||
var canvas = $(this).get(0);
|
||||
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
|
||||
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
|
||||
resized = true;
|
||||
}
|
||||
|
||||
if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) {
|
||||
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
|
||||
resized = true;
|
||||
}
|
||||
Object.keys(audioLevelCanvasCache).forEach(function (id) {
|
||||
audioLevelCanvasCache[id].width = canvasWidth;
|
||||
audioLevelCanvasCache[id].height = canvasHeight;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (resized)
|
||||
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
|
||||
audioLevelCanvasCache[resourceJid].width =
|
||||
width + interfaceConfig.CANVAS_EXTRA;
|
||||
audioLevelCanvasCache[resourceJid].height =
|
||||
height + interfaceConfig.CANVAS_EXTRA;
|
||||
});
|
||||
});
|
||||
|
||||
return my;
|
||||
|
||||
})(AudioLevels || {});
|
||||
|
||||
module.exports = AudioLevels;
|
||||
export default AudioLevels;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Utility class for drawing canvas shapes.
|
||||
*/
|
||||
var CanvasUtil = (function(my) {
|
||||
const CanvasUtil = {
|
||||
|
||||
/**
|
||||
* 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 glowWidth the width of the glow
|
||||
*/
|
||||
my.drawRoundRectGlow
|
||||
= function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
|
||||
drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
|
||||
|
||||
// Save the previous state of the context.
|
||||
drawContext.save();
|
||||
|
@ -73,14 +72,14 @@ var CanvasUtil = (function(my) {
|
|||
|
||||
// Restore the previous context state.
|
||||
drawContext.restore();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Clones the given 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,
|
||||
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
|
||||
|
@ -103,9 +102,7 @@ var CanvasUtil = (function(my) {
|
|||
|
||||
//return the new canvas
|
||||
return newCanvas;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return my;
|
||||
})(CanvasUtil || {});
|
||||
|
||||
module.exports = CanvasUtil;
|
||||
export default 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*/
|
||||
|
||||
var XMPP = require('../../xmpp/xmpp');
|
||||
var Moderator = require('../../xmpp/moderator');
|
||||
|
||||
//FIXME: use LoginDialog to add retries to XMPP.connect method used when
|
||||
// anonymous domain is not enabled
|
||||
var messageHandler = require('../util/MessageHandler');
|
||||
|
||||
/**
|
||||
* Creates new <tt>Dialog</tt> instance.
|
||||
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt> 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.
|
||||
* @constructor
|
||||
* Build html for "password required" dialog.
|
||||
* @returns {string} html string
|
||||
*/
|
||||
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;
|
||||
|
||||
var stop = false;
|
||||
|
||||
var connection = APP.xmpp.createConnection();
|
||||
|
||||
var message = '<h2 data-i18n="dialog.passwordRequired">';
|
||||
message += APP.translation.translateString("dialog.passwordRequired");
|
||||
message += '</h2>' +
|
||||
'<input name="username" type="text" ';
|
||||
if (config.hosts.authdomain) {
|
||||
message += 'placeholder="user identity" autofocus>';
|
||||
} else {
|
||||
message += 'placeholder="user@domain.net" autofocus>';
|
||||
/**
|
||||
* Convert provided id to jid if it's not jid yet.
|
||||
* @param {string} id user id or jid
|
||||
* @returns {string} jid
|
||||
*/
|
||||
function toJid(id) {
|
||||
if (id.indexOf("@") >= 0) {
|
||||
return id;
|
||||
}
|
||||
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('@');
|
||||
if (config.hosts.authdomain) {
|
||||
jid += config.hosts.authdomain;
|
||||
} else {
|
||||
jid += config.hosts.domain;
|
||||
}
|
||||
|
||||
var cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
|
||||
return jid;
|
||||
}
|
||||
|
||||
var states = {
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
const states = {
|
||||
login: {
|
||||
html: message,
|
||||
buttons: [
|
||||
{ title: okButton, value: true},
|
||||
{ title: cancelButton, value: false}
|
||||
],
|
||||
html: getPasswordInputHtml(),
|
||||
buttons: loginButtons,
|
||||
focus: ':input:first',
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v) {
|
||||
var jid = f.username;
|
||||
var password = f.password;
|
||||
let jid = f.username;
|
||||
let password = f.password;
|
||||
if (jid && password) {
|
||||
stop = false;
|
||||
if (jid.indexOf("@") < 0) {
|
||||
jid = jid.concat('@');
|
||||
if (config.hosts.authdomain) {
|
||||
jid += config.hosts.authdomain;
|
||||
} else {
|
||||
jid += config.hosts.domain;
|
||||
}
|
||||
}
|
||||
connection.reset();
|
||||
connDialog.goToState('connecting');
|
||||
connection.connect(jid, password, stateHandler);
|
||||
successCallback(toJid(jid), password);
|
||||
}
|
||||
} else {
|
||||
// User cancelled
|
||||
stop = true;
|
||||
callback();
|
||||
cancelCallback();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -82,115 +109,23 @@ function Dialog(callback, obtainSession) {
|
|||
finished: {
|
||||
title: APP.translation.translateString('dialog.error'),
|
||||
html: '<div id="errorMessage"></div>',
|
||||
buttons: [
|
||||
{
|
||||
title: APP.translation.translateString('dialog.retry'),
|
||||
value: 'retry'
|
||||
},
|
||||
{
|
||||
title: APP.translation.translateString('dialog.Cancel'),
|
||||
value: 'cancel'
|
||||
},
|
||||
],
|
||||
buttons: finishedButtons,
|
||||
defaultButton: 0,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 'retry')
|
||||
if (v === 'retry') {
|
||||
connDialog.goToState('login');
|
||||
else
|
||||
callback();
|
||||
} else {
|
||||
// User cancelled
|
||||
cancelCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var connDialog
|
||||
= APP.UI.messageHandler.openDialogWithStates(states,
|
||||
{ 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;
|
||||
}
|
||||
};
|
||||
var connDialog = messageHandler.openDialogWithStates(
|
||||
states, { persistent: true, closeText: '' }, null
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays error message in 'finished' state which allows either to cancel
|
||||
|
@ -199,42 +134,103 @@ function Dialog(callback, obtainSession) {
|
|||
*/
|
||||
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);
|
||||
|
||||
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.
|
||||
*/
|
||||
this.close = function () {
|
||||
stop = true;
|
||||
connDialog.close();
|
||||
};
|
||||
}
|
||||
|
||||
var LoginDialog = {
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Displays login prompt used to establish new XMPP connection. Given
|
||||
* <tt>callback(Strophe.Connection, Strophe.Status)</tt> function will be
|
||||
* called when we connect successfully(status === CONNECTED) or when we fail
|
||||
* to do so. On connection failure program can call Dialog.close() method in
|
||||
* order to cancel or do nothing to let the user retry.
|
||||
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt>
|
||||
* 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 new auth dialog for JitsiConnection.
|
||||
*
|
||||
* @param {function(jid, password)} successCallback
|
||||
* @param {function} [cancelCallback] callback to invoke if user canceled.
|
||||
*
|
||||
* @returns {LoginDialog}
|
||||
*/
|
||||
show: function (callback, obtainSession) {
|
||||
return new Dialog(callback, obtainSession);
|
||||
showAuthDialog: function (successCallback, cancelCallback) {
|
||||
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 */
|
||||
var Settings = require("../../settings/Settings");
|
||||
/* global MD5, config, interfaceConfig */
|
||||
|
||||
var users = {};
|
||||
let users = {};
|
||||
|
||||
var Avatar = {
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Sets the user's avatar in the settings menu(if local user), contact list
|
||||
* and thumbnail
|
||||
* @param jid jid of the user
|
||||
* @param id email or userID to be used as a hash
|
||||
* @param id id of the user
|
||||
* @param email email or nickname to be used as a hash
|
||||
*/
|
||||
setUserAvatar: function (jid, id) {
|
||||
if (id) {
|
||||
if (users[jid] === id) {
|
||||
setUserAvatar: function (id, email) {
|
||||
if (email) {
|
||||
if (users[id] === email) {
|
||||
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,
|
||||
* identified by its jid
|
||||
* @param jid
|
||||
* @param jid full MUC jid of the user for whom we want to obtain avatar URL
|
||||
* identified by its id.
|
||||
* @param {string} userId user id
|
||||
*/
|
||||
getAvatarUrl: function (jid) {
|
||||
getAvatarUrl: function (userId) {
|
||||
if (config.disableThirdPartyRequests) {
|
||||
return 'images/avatar2.png';
|
||||
} else {
|
||||
if (!jid) {
|
||||
console.error("Get avatar - jid is undefined");
|
||||
return null;
|
||||
}
|
||||
var id = users[jid];
|
||||
|
||||
// If the ID looks like an email, we'll use gravatar.
|
||||
// Otherwise, it's a random avatar, and we'll use the configured
|
||||
// URL.
|
||||
var random = !id || id.indexOf('@') < 0;
|
||||
|
||||
if (!id) {
|
||||
console.warn(
|
||||
"No avatar stored yet for " + jid + " - using JID as ID");
|
||||
id = jid;
|
||||
}
|
||||
id = MD5.hexdigest(id.trim().toLowerCase());
|
||||
|
||||
// Default to using gravatar.
|
||||
var urlPref = 'https://www.gravatar.com/avatar/';
|
||||
var urlSuf = "?d=wavatar&size=100";
|
||||
|
||||
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
|
||||
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
|
||||
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
|
||||
}
|
||||
|
||||
return urlPref + id + urlSuf;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
console.error("Get avatar - id is undefined");
|
||||
return null;
|
||||
}
|
||||
|
||||
let avatarId = users[userId];
|
||||
|
||||
// If the ID looks like an email, we'll use gravatar.
|
||||
// Otherwise, it's a random avatar, and we'll use the configured
|
||||
// URL.
|
||||
let random = !avatarId || avatarId.indexOf('@') < 0;
|
||||
|
||||
if (!avatarId) {
|
||||
console.warn(
|
||||
`No avatar stored yet for ${userId} - using ID as avatar ID`);
|
||||
avatarId = userId;
|
||||
}
|
||||
avatarId = MD5.hexdigest(avatarId.trim().toLowerCase());
|
||||
|
||||
// Default to using gravatar.
|
||||
let urlPref = 'https://www.gravatar.com/avatar/';
|
||||
let urlSuf = "?d=wavatar&size=100";
|
||||
|
||||
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
|
||||
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
|
||||
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
|
||||
}
|
||||
|
||||
return urlPref + avatarId + urlSuf;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Avatar;
|
||||
|
|
|
@ -1,61 +1,20 @@
|
|||
/* global $, config,
|
||||
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";
|
||||
/* global $ */
|
||||
|
||||
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() {
|
||||
if ($('#etherpad>iframe').length) {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
var availableHeight
|
||||
= window.innerHeight - remoteVideos.outerHeight();
|
||||
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);
|
||||
});
|
||||
}
|
||||
const options = $.param({
|
||||
showControns: true,
|
||||
showChat: false,
|
||||
showLineNumbers: true,
|
||||
useMonospaceFont: false
|
||||
});
|
||||
|
||||
function bubbleIframeMouseMove(iframe){
|
||||
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
||||
|
@ -71,8 +30,8 @@ function bubbleIframeMouseMove(iframe){
|
|||
e.detail,
|
||||
e.screenX,
|
||||
e.screenY,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.ctrlKey,
|
||||
e.altKey,
|
||||
e.shiftKey,
|
||||
|
@ -84,48 +43,140 @@ function bubbleIframeMouseMove(iframe){
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Etherpad frame width.
|
||||
*/
|
||||
const DEFAULT_WIDTH = 640;
|
||||
/**
|
||||
* Default Etherpad frame height.
|
||||
*/
|
||||
const DEFAULT_HEIGHT = 480;
|
||||
|
||||
var Etherpad = {
|
||||
/**
|
||||
* Initializes the etherpad.
|
||||
*/
|
||||
init: function (name) {
|
||||
const EtherpadContainerType = "etherpad";
|
||||
|
||||
if (config.etherpad_base && !etherpadName && name) {
|
||||
/**
|
||||
* Container for Etherpad iframe.
|
||||
*/
|
||||
class Etherpad extends LargeContainer {
|
||||
constructor (domain, name) {
|
||||
super();
|
||||
|
||||
domain = config.etherpad_base;
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
etherpadName = name;
|
||||
iframe.src = domain + name + '?' + options;
|
||||
iframe.frameBorder = 0;
|
||||
iframe.scrolling = "no";
|
||||
iframe.width = DEFAULT_WIDTH;
|
||||
iframe.height = DEFAULT_HEIGHT;
|
||||
iframe.setAttribute('style', 'visibility: hidden;');
|
||||
|
||||
enableEtherpadButton();
|
||||
this.container.appendChild(iframe);
|
||||
|
||||
/**
|
||||
* Resizes the etherpad, when the window is resized.
|
||||
*/
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
}
|
||||
},
|
||||
iframe.onload = function() {
|
||||
document.domain = document.domain;
|
||||
bubbleIframeMouseMove(iframe);
|
||||
|
||||
/**
|
||||
* Opens/hides the Etherpad.
|
||||
*/
|
||||
toggleEtherpad: function (isPresentation) {
|
||||
if (!etherpadIFrame)
|
||||
createIFrame();
|
||||
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);
|
||||
|
||||
if(VideoLayout.getLargeVideoState() === "etherpad")
|
||||
{
|
||||
VideoLayout.setLargeVideoState("video");
|
||||
}
|
||||
else
|
||||
{
|
||||
VideoLayout.setLargeVideoState("etherpad");
|
||||
}
|
||||
resize();
|
||||
const inner = doc.getElementsByName("ace_inner")[0];
|
||||
bubbleIframeMouseMove(inner);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
this.iframe = iframe;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Etherpad;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Etherpad frame.
|
||||
*/
|
||||
openEtherpad () {
|
||||
this.etherpad = new Etherpad(this.domain, this.name);
|
||||
VideoLayout.addLargeVideoContainer(
|
||||
EtherpadContainerType,
|
||||
this.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");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var VideoLayout = require("../videolayout/VideoLayout");
|
||||
var messageHandler = require("../util/MessageHandler");
|
||||
var PreziPlayer = require("./PreziPlayer");
|
||||
|
||||
var preziPlayer = null;
|
||||
/* global $, APP */
|
||||
/* jshint -W101 */
|
||||
|
||||
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) {
|
||||
|
||||
if (visible) {
|
||||
VideoLayout.setLargeVideoState("prezi");
|
||||
}
|
||||
else {
|
||||
VideoLayout.setLargeVideoState("video");
|
||||
}
|
||||
}
|
||||
|
||||
var Prezi = {
|
||||
|
||||
|
||||
/**
|
||||
* Reloads the current presentation.
|
||||
*/
|
||||
reloadPresentation: function() {
|
||||
var iframe = document.getElementById(preziPlayer.options.preziId);
|
||||
iframe.src = iframe.src;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if the presentation is visible, <tt>false</tt> -
|
||||
* otherwise.
|
||||
*/
|
||||
isPresentationVisible: function () {
|
||||
return ($('#presentation>iframe') != null
|
||||
&& $('#presentation>iframe').css('opacity') == 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the Prezi dialog, from which the user could choose a presentation
|
||||
* to load.
|
||||
*/
|
||||
openPreziDialog: function() {
|
||||
var myprezi = APP.xmpp.getPrezi();
|
||||
if (myprezi) {
|
||||
messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
|
||||
null,
|
||||
"dialog.removePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if(v) {
|
||||
APP.xmpp.removePreziFromPresence();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (preziPlayer != null) {
|
||||
messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
|
||||
null, "dialog.sharePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Ok",
|
||||
function(e,v,m,f) {
|
||||
$.prompt.close();
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
var html = APP.translation.generateTranslationHTML(
|
||||
"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: {
|
||||
html: '<h2>' + html + '</h2>' +
|
||||
'<input name="preziUrl" type="text" ' +
|
||||
'data-i18n="[placeholder]defaultPreziLink" data-i18n-options=\'' +
|
||||
JSON.stringify({"url": "http://prezi.com/wz7vhjycl7e6/my-prezi"}) +
|
||||
'\' placeholder="' + defaultUrl + '" autofocus>',
|
||||
persistent: false,
|
||||
buttons: buttons,
|
||||
focus: ':input:first',
|
||||
defaultButton: 0,
|
||||
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',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 0)
|
||||
$.prompt.close();
|
||||
else
|
||||
$.prompt.goToState('state0');
|
||||
}
|
||||
}
|
||||
};
|
||||
messageHandler.openDialogWithStates(openPreziState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Default Prezi frame width.
|
||||
*/
|
||||
function presentationAdded(event, jid, presUrl, currentSlide) {
|
||||
console.log("presentation added", presUrl);
|
||||
|
||||
var presId = getPresentationId(presUrl);
|
||||
|
||||
var elementId = 'participant_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '_' + 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')
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer = new PreziPlayer(
|
||||
'presentation',
|
||||
{preziId: presId,
|
||||
width: getPresentationWidth(),
|
||||
height: getPresentationHeihgt(),
|
||||
controls: controlsEnabled,
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const DEFAULT_WIDTH = 640;
|
||||
/**
|
||||
* A presentation has been removed.
|
||||
*
|
||||
* @param event the event indicating the remove of a presentation
|
||||
* @param jid the jid for which the presentation was removed
|
||||
* @param the url of the presentation
|
||||
* Default Prezi frame height.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
};
|
||||
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) {
|
||||
var regex = /^[a-z0-9-_\/&\?=;]+$/i;
|
||||
return regex.test(unsafeText);
|
||||
return alphanumRegex.test(unsafeText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation id from the given url.
|
||||
* @param {string} url Prezi link
|
||||
* @returns {string} presentation id
|
||||
*/
|
||||
function getPresentationId (presUrl) {
|
||||
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
|
||||
return presIdTmp.substring(0, presIdTmp.indexOf('/'));
|
||||
function getPresentationId (url) {
|
||||
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||
return presId.substring(0, presId.indexOf('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation width.
|
||||
* Checks if given string is Prezi url.
|
||||
* @param {string} url string to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function getPresentationWidth() {
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
||||
var availableHeight = getPresentationHeihgt();
|
||||
|
||||
var aspectRatio = 16.0 / 9.0;
|
||||
if (availableHeight < availableWidth / aspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
||||
function isPreziLink(url) {
|
||||
if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
|
||||
return false;
|
||||
}
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation height.
|
||||
*/
|
||||
function getPresentationHeihgt() {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
return window.innerHeight - remoteVideos.outerHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the presentation iframe.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#presentation>iframe')) {
|
||||
$('#presentation>iframe').width(getPresentationWidth());
|
||||
$('#presentation>iframe').height(getPresentationHeihgt());
|
||||
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||
if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation has been removed.
|
||||
* Notify user that other user if already sharing Prezi.
|
||||
*/
|
||||
$(document).bind('presentationremoved.muc', presentationRemoved);
|
||||
function notifyOtherIsSharingPrezi() {
|
||||
messageHandler.openMessageDialog(
|
||||
"dialog.sharePreziTitle",
|
||||
"dialog.sharePreziMsg"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation has been added.
|
||||
* Ask user if he want to close Prezi he's sharing.
|
||||
*/
|
||||
$(document).bind('presentationadded.muc', presentationAdded);
|
||||
function proposeToClosePrezi() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
messageHandler.openTwoButtonDialog(
|
||||
"dialog.removePreziTitle",
|
||||
null,
|
||||
"dialog.removePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if (v) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Indicates presentation slide change.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for Prezi url to share with others.
|
||||
* Dialog validates client input to allow only Prezi urls.
|
||||
*/
|
||||
$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
|
||||
if (preziPlayer && preziPlayer.getCurrentStep() != current) {
|
||||
preziPlayer.flyToStep(current);
|
||||
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
|
||||
);
|
||||
|
||||
var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
|
||||
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
|
||||
preziPlayer.flyToStep(current, i);
|
||||
return new Promise(function (resolve, reject) {
|
||||
let dialog = messageHandler.openDialogWithStates({
|
||||
state0: {
|
||||
html: `
|
||||
<h2>${title}</h2>
|
||||
<input name="preziUrl" type="text"
|
||||
data-i18n="[placeholder]defaultPreziLink"
|
||||
data-i18n-options="${JSON.stringify(i18nOptions)}"
|
||||
placeholder="${defaultUrl}" autofocus>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: shareButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (!v) {
|
||||
reject('cancelled');
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let preziUrl = f.preziUrl;
|
||||
if (!preziUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl));
|
||||
|
||||
if (!isPreziLink(urlValue)) {
|
||||
dialog.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve(urlValue);
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export const PreziContainerType = "prezi";
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
);
|
||||
this.preziPlayer = preziPlayer;
|
||||
this.$iframe = $(preziPlayer.iframe);
|
||||
|
||||
this.$iframe.attr('id', preziId);
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) {
|
||||
console.log("prezi status", value);
|
||||
if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) {
|
||||
preziPlayer.flyToStep(slide);
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) {
|
||||
console.log("event value", value);
|
||||
onSlideChanged(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Prezi slide.
|
||||
* @param {number} slide slide to show
|
||||
*/
|
||||
goToSlide (slide) {
|
||||
if (this.preziPlayer.getCurrentStep() === slide) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preziPlayer.flyToStep(slide);
|
||||
|
||||
let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps();
|
||||
if (!animationStepsArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) {
|
||||
this.preziPlayer.flyToStep(slide, i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
/**
|
||||
* Show or hide "reload presentation" button.
|
||||
* @param {boolean} show
|
||||
*/
|
||||
showReloadBtn (show) {
|
||||
this.reloadBtn.css('display', show ? 'inline-block' : 'none');
|
||||
}
|
||||
|
||||
module.exports = Prezi;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager of Prezi frames.
|
||||
*/
|
||||
export default class PreziManager {
|
||||
constructor (emitter) {
|
||||
this.emitter = emitter;
|
||||
|
||||
this.userId = null;
|
||||
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
|
||||
*/
|
||||
isSharing (id) {
|
||||
return this.userId === id;
|
||||
}
|
||||
|
||||
handlePreziButtonClicked () {
|
||||
if (!this.isPresenting) {
|
||||
requestPreziLink().then(
|
||||
url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0),
|
||||
err => console.error('PREZI CANCELED', err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMyPrezi) {
|
||||
proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI));
|
||||
} else {
|
||||
notifyOtherIsSharingPrezi();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload current Prezi frame.
|
||||
*/
|
||||
reloadPresentation () {
|
||||
if (!this.prezi) {
|
||||
return;
|
||||
}
|
||||
let iframe = this.prezi.$iframe[0];
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,298 +1,290 @@
|
|||
/* global PreziPlayer */
|
||||
/* 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.CURRENT_STEP = 'currentStep';
|
||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||
PreziPlayer.STATUS_LOADING = 'loading';
|
||||
PreziPlayer.STATUS_READY = 'ready';
|
||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||
PreziPlayer.domain = "https://prezi.com";
|
||||
PreziPlayer.path = "/player/";
|
||||
PreziPlayer.players = {};
|
||||
PreziPlayer.binded_methods = ['changesHandler'];
|
||||
PreziPlayer.API_VERSION = 1;
|
||||
PreziPlayer.CURRENT_STEP = 'currentStep';
|
||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||
PreziPlayer.STATUS_LOADING = 'loading';
|
||||
PreziPlayer.STATUS_READY = 'ready';
|
||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||
PreziPlayer.domain = "https://prezi.com";
|
||||
PreziPlayer.path = "/player/";
|
||||
PreziPlayer.players = {};
|
||||
PreziPlayer.binded_methods = ['changesHandler'];
|
||||
|
||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||
for(var i=0; i<optionArray.length; i++) {
|
||||
var optionSet = optionArray[i];
|
||||
new PreziPlayer(optionSet.id, optionSet);
|
||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||
for(var i=0; i<optionArray.length; i++) {
|
||||
var optionSet = optionArray[i];
|
||||
new PreziPlayer(optionSet.id, optionSet);
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.messageReceived = function(event){
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.messageReceived = function(event){
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
/*jshint -W004 */
|
||||
function PreziPlayer(id, options) {
|
||||
/*jshint +W004 */
|
||||
var params, paramString = "", _this = this;
|
||||
if (PreziPlayer.players[id]){
|
||||
PreziPlayer.players[id].destroy();
|
||||
}
|
||||
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
|
||||
var method_name = PreziPlayer.binded_methods[i];
|
||||
_this[method_name] = __bind(_this[method_name], _this);
|
||||
}
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
this.values = {'status': PreziPlayer.STATUS_LOADING};
|
||||
this.values[PreziPlayer.CURRENT_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_OBJECT] = null;
|
||||
this.callbacks = [];
|
||||
this.id = id;
|
||||
this.embedTo = document.getElementById(id);
|
||||
if (!this.embedTo) {
|
||||
throw "The element id is not available.";
|
||||
}
|
||||
this.iframe = document.createElement('iframe');
|
||||
params = [
|
||||
{ name: 'oid', value: options.preziId },
|
||||
{ name: 'explorable', value: options.explorable ? 1 : 0 },
|
||||
{ name: 'controls', value: options.controls ? 1 : 0 }
|
||||
];
|
||||
for (i=0; i<params.length; i++) {
|
||||
var param = params[i];
|
||||
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
|
||||
}
|
||||
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
|
||||
this.iframe.frameBorder = 0;
|
||||
this.iframe.scrolling = "no";
|
||||
this.iframe.width = options.width || 640;
|
||||
this.iframe.height = options.height || 480;
|
||||
this.embedTo.innerHTML = '';
|
||||
// JITSI: IN CASE SOMETHING GOES WRONG.
|
||||
try {
|
||||
this.embedTo.appendChild(this.iframe);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("CATCH ERROR");
|
||||
}
|
||||
function PreziPlayer(id, options) {
|
||||
/*jshint +W004 */
|
||||
var params, paramString = "", _this = this;
|
||||
if (PreziPlayer.players[id]){
|
||||
PreziPlayer.players[id].destroy();
|
||||
}
|
||||
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
|
||||
var method_name = PreziPlayer.binded_methods[i];
|
||||
_this[method_name] = __bind(_this[method_name], _this);
|
||||
}
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
this.values = {'status': PreziPlayer.STATUS_LOADING};
|
||||
this.values[PreziPlayer.CURRENT_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_OBJECT] = null;
|
||||
this.callbacks = [];
|
||||
this.id = id;
|
||||
this.embedTo = document.getElementById(id);
|
||||
if (!this.embedTo) {
|
||||
throw "The element id is not available.";
|
||||
}
|
||||
this.iframe = document.createElement('iframe');
|
||||
params = [
|
||||
{ name: 'oid', value: options.preziId },
|
||||
{ name: 'explorable', value: options.explorable ? 1 : 0 },
|
||||
{ name: 'controls', value: options.controls ? 1 : 0 }
|
||||
];
|
||||
for (i=0; i<params.length; i++) {
|
||||
var param = params[i];
|
||||
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
|
||||
}
|
||||
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
|
||||
this.iframe.frameBorder = 0;
|
||||
this.iframe.scrolling = "no";
|
||||
this.iframe.width = options.width || 640;
|
||||
this.iframe.height = options.height || 480;
|
||||
this.embedTo.innerHTML = '';
|
||||
// JITSI: IN CASE SOMETHING GOES WRONG.
|
||||
try {
|
||||
this.embedTo.appendChild(this.iframe);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("CATCH ERROR");
|
||||
}
|
||||
|
||||
// JITSI: Increase interval from 200 to 500, which fixes prezi
|
||||
// crashes for us.
|
||||
this.initPollInterval = setInterval(function(){
|
||||
_this.sendMessage({'action': 'init'});
|
||||
}, 500);
|
||||
PreziPlayer.players[id] = this;
|
||||
}
|
||||
// JITSI: Increase interval from 200 to 500, which fixes prezi
|
||||
// crashes for us.
|
||||
this.initPollInterval = setInterval(function(){
|
||||
_this.sendMessage({'action': 'init'});
|
||||
}, 500);
|
||||
PreziPlayer.players[id] = this;
|
||||
}
|
||||
|
||||
PreziPlayer.prototype.changesHandler = function(message) {
|
||||
var key, value, j, item;
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
for (key in message.data) {
|
||||
if (message.data.hasOwnProperty(key)){
|
||||
value = message.data[key];
|
||||
this.values[key] = value;
|
||||
for (j=0; j<this.callbacks.length; j++) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === key + "Change"){
|
||||
item.callback({type: item.event, value: value});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.destroy = function() {
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
this.embedTo.innerHTML = '';
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.sendMessage = function(message) {
|
||||
if (this.options.debug === true) {
|
||||
if (console && console.log) console.log('sent', message);
|
||||
}
|
||||
message.version = PreziPlayer.API_VERSION;
|
||||
message.id = this.id;
|
||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToNextStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToNextStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToPrevStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||
var obj = this;
|
||||
// check animation_step
|
||||
if (animation_step > 0 &&
|
||||
obj.values.animationCountOnSteps &&
|
||||
obj.values.animationCountOnSteps[step] <= animation_step) {
|
||||
animation_step = obj.values.animationCountOnSteps[step];
|
||||
}
|
||||
// jump to animation steps by calling flyToNextStep()
|
||||
function doAnimationSteps() {
|
||||
if (obj.values.isMoving) {
|
||||
setTimeout(doAnimationSteps, 100); // wait until the flight ends
|
||||
return;
|
||||
}
|
||||
while (animation_step-- > 0) {
|
||||
obj.flyToNextStep(); // do the animation steps
|
||||
}
|
||||
}
|
||||
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
|
||||
// jump to the step
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToStep', step]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToObject', objectId]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['startAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.stop = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['stopAutoPlay']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['pauseAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentStep = function() {
|
||||
return this.values.currentStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||
return this.values.currentAnimationStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentObject = function() {
|
||||
return this.values.currentObject;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStatus = function() {
|
||||
return this.values.status;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.isPlaying = function() {
|
||||
return this.values.isAutoPlaying;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStepCount = function() {
|
||||
return this.values.stepCount;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||
return this.values.animationCountOnSteps;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getTitle = function() {
|
||||
return this.values.title;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||
for (var parameter in dims) {
|
||||
this.iframe[parameter] = dims[parameter];
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getDimensions = function() {
|
||||
return {
|
||||
width: parseInt(this.iframe.width, 10),
|
||||
height: parseInt(this.iframe.height, 10)
|
||||
};
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.on = function(event, callback) {
|
||||
this.callbacks.push({
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.off = function(event, callback) {
|
||||
var j, item;
|
||||
if (event === undefined) {
|
||||
this.callbacks = [];
|
||||
}
|
||||
j = this.callbacks.length;
|
||||
while (j--) {
|
||||
PreziPlayer.prototype.changesHandler = function(message) {
|
||||
var key, value, j, item;
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
for (key in message.data) {
|
||||
if (message.data.hasOwnProperty(key)){
|
||||
value = message.data[key];
|
||||
this.values[key] = value;
|
||||
for (j=0; j<this.callbacks.length; j++) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === event && (callback === undefined || item.callback === callback)){
|
||||
this.callbacks.splice(j, 1);
|
||||
if (item && item.event === key + "Change"){
|
||||
item.callback({type: item.event, value: value});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return PreziPlayer;
|
||||
PreziPlayer.prototype.destroy = function() {
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
this.embedTo.innerHTML = '';
|
||||
};
|
||||
|
||||
})();
|
||||
PreziPlayer.prototype.sendMessage = function(message) {
|
||||
if (this.options.debug === true) {
|
||||
if (console && console.log) console.log('sent', message);
|
||||
}
|
||||
message.version = PreziPlayer.API_VERSION;
|
||||
message.id = this.id;
|
||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||
};
|
||||
|
||||
})();
|
||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToNextStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToNextStep']
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = PreziPlayer;
|
||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToPrevStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||
var obj = this;
|
||||
// check animation_step
|
||||
if (animation_step > 0 &&
|
||||
obj.values.animationCountOnSteps &&
|
||||
obj.values.animationCountOnSteps[step] <= animation_step) {
|
||||
animation_step = obj.values.animationCountOnSteps[step];
|
||||
}
|
||||
// jump to animation steps by calling flyToNextStep()
|
||||
function doAnimationSteps() {
|
||||
if (obj.values.isMoving) {
|
||||
setTimeout(doAnimationSteps, 100); // wait until the flight ends
|
||||
return;
|
||||
}
|
||||
while (animation_step-- > 0) {
|
||||
obj.flyToNextStep(); // do the animation steps
|
||||
}
|
||||
}
|
||||
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
|
||||
// jump to the step
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToStep', step]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToObject', objectId]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['startAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.stop = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['stopAutoPlay']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['pauseAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentStep = function() {
|
||||
return this.values.currentStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||
return this.values.currentAnimationStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentObject = function() {
|
||||
return this.values.currentObject;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStatus = function() {
|
||||
return this.values.status;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.isPlaying = function() {
|
||||
return this.values.isAutoPlaying;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStepCount = function() {
|
||||
return this.values.stepCount;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||
return this.values.animationCountOnSteps;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getTitle = function() {
|
||||
return this.values.title;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||
for (var parameter in dims) {
|
||||
this.iframe[parameter] = dims[parameter];
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getDimensions = function() {
|
||||
return {
|
||||
width: parseInt(this.iframe.width, 10),
|
||||
height: parseInt(this.iframe.height, 10)
|
||||
};
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.on = function(event, callback) {
|
||||
this.callbacks.push({
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.off = function(event, callback) {
|
||||
var j, item;
|
||||
if (event === undefined) {
|
||||
this.callbacks = [];
|
||||
}
|
||||
j = this.callbacks.length;
|
||||
while (j--) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === event && (callback === undefined || item.callback === callback)){
|
||||
this.callbacks.splice(j, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||
}
|
||||
|
||||
window.PreziPlayer = PreziPlayer;
|
||||
|
||||
export default PreziPlayer;
|
||||
|
|
|
@ -1,102 +1,100 @@
|
|||
/* global require, $ */
|
||||
var Chat = require("./chat/Chat");
|
||||
var ContactList = require("./contactlist/ContactList");
|
||||
var Settings = require("./../../settings/Settings");
|
||||
var SettingsMenu = require("./settings/SettingsMenu");
|
||||
var VideoLayout = require("../videolayout/VideoLayout");
|
||||
var ToolbarToggler = require("../toolbars/ToolbarToggler");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var LargeVideo = require("../videolayout/LargeVideo");
|
||||
import Chat from "./chat/Chat";
|
||||
import ContactList from "./contactlist/ContactList";
|
||||
import Settings from "./../../settings/Settings";
|
||||
import SettingsMenu from "./settings/SettingsMenu";
|
||||
import VideoLayout from "../videolayout/VideoLayout";
|
||||
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
const buttons = {
|
||||
'#chatspace': '#chatBottomButton',
|
||||
'#contactlist': '#contactListButton',
|
||||
'#settingsmenu': '#toolbar_button_settings'
|
||||
};
|
||||
|
||||
var currentlyOpen = null;
|
||||
|
||||
/**
|
||||
* Toggles the windows in the side panel
|
||||
* @param object the window that should be shown
|
||||
* @param selector the selector for the element containing the panel
|
||||
* @param onOpenComplete function to be called when the panel is 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
|
||||
*/
|
||||
function toggle (object, selector, onOpenComplete, onOpen, onClose) {
|
||||
UIUtil.buttonClick(buttons[selector], "active");
|
||||
|
||||
if (object.isVisible()) {
|
||||
$("#toast-container").animate({
|
||||
right: 5
|
||||
}, {
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
$(selector).hide("slide", {
|
||||
direction: "right",
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
if(typeof onClose === "function") {
|
||||
onClose();
|
||||
}
|
||||
|
||||
currentlyOpen = null;
|
||||
} else {
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
// video mode.
|
||||
if (VideoLayout.isLargeVideoVisible()) {
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
}
|
||||
|
||||
if (currentlyOpen) {
|
||||
var current = $(currentlyOpen);
|
||||
UIUtil.buttonClick(buttons[currentlyOpen], "active");
|
||||
current.css('z-index', 4);
|
||||
setTimeout(function () {
|
||||
current.css('display', 'none');
|
||||
current.css('z-index', 5);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
$("#toast-container").animate({
|
||||
right: (UIUtil.getSidePanelSize()[0] + 5)
|
||||
}, {
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
$(selector).show("slide", {
|
||||
direction: "right",
|
||||
queue: false,
|
||||
duration: 500,
|
||||
complete: onOpenComplete
|
||||
});
|
||||
if(typeof onOpen === "function") {
|
||||
onOpen();
|
||||
}
|
||||
|
||||
currentlyOpen = selector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggler for the chat, contact list, settings menu, etc..
|
||||
*/
|
||||
var PanelToggler = (function(my) {
|
||||
|
||||
var currentlyOpen = null;
|
||||
var buttons = {
|
||||
'#chatspace': '#chatBottomButton',
|
||||
'#contactlist': '#contactListButton',
|
||||
'#settingsmenu': '#toolbar_button_settings'
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the windows in the side panel
|
||||
* @param object the window that should be shown
|
||||
* @param selector the selector for the element containing the panel
|
||||
* @param onOpenComplete function to be called when the panel is 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
|
||||
*/
|
||||
var toggle = function(object, selector, onOpenComplete, onOpen, onClose) {
|
||||
UIUtil.buttonClick(buttons[selector], "active");
|
||||
|
||||
if (object.isVisible()) {
|
||||
$("#toast-container").animate({
|
||||
right: '5px'
|
||||
},
|
||||
{
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
$(selector).hide("slide", {
|
||||
direction: "right",
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
if(typeof onClose === "function") {
|
||||
onClose();
|
||||
}
|
||||
|
||||
currentlyOpen = null;
|
||||
}
|
||||
else {
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
// video mode.
|
||||
if (LargeVideo.isLargeVideoVisible()) {
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
}
|
||||
|
||||
if(currentlyOpen) {
|
||||
var current = $(currentlyOpen);
|
||||
UIUtil.buttonClick(buttons[currentlyOpen], "active");
|
||||
current.css('z-index', 4);
|
||||
setTimeout(function () {
|
||||
current.css('display', 'none');
|
||||
current.css('z-index', 5);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
$("#toast-container").animate({
|
||||
right: (PanelToggler.getPanelSize()[0] + 5) + 'px'
|
||||
},
|
||||
{
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
$(selector).show("slide", {
|
||||
direction: "right",
|
||||
queue: false,
|
||||
duration: 500,
|
||||
complete: onOpenComplete
|
||||
});
|
||||
if(typeof onOpen === "function") {
|
||||
onOpen();
|
||||
}
|
||||
|
||||
currentlyOpen = selector;
|
||||
}
|
||||
};
|
||||
var PanelToggler = {
|
||||
|
||||
/**
|
||||
* Opens / closes the chat area.
|
||||
*/
|
||||
my.toggleChat = function() {
|
||||
var chatCompleteFunction = Chat.isVisible() ?
|
||||
function() {} : function () {
|
||||
Chat.scrollChatToBottom();
|
||||
$('#chatspace').trigger('shown');
|
||||
};
|
||||
toggleChat () {
|
||||
var chatCompleteFunction = Chat.isVisible()
|
||||
? function () {}
|
||||
: function () {
|
||||
Chat.scrollChatToBottom();
|
||||
$('#chatspace').trigger('shown');
|
||||
};
|
||||
|
||||
VideoLayout.resizeVideoArea(!Chat.isVisible(), chatCompleteFunction);
|
||||
|
||||
|
@ -112,16 +110,24 @@ var PanelToggler = (function(my) {
|
|||
}
|
||||
},
|
||||
null,
|
||||
Chat.resizeChat,
|
||||
() => this.resizeChat(),
|
||||
null);
|
||||
};
|
||||
},
|
||||
|
||||
resizeChat () {
|
||||
let [width, height] = UIUtil.getSidePanelSize();
|
||||
Chat.resizeChat(width, height);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens / closes the contact list area.
|
||||
*/
|
||||
my.toggleContactList = function () {
|
||||
var completeFunction = ContactList.isVisible() ?
|
||||
function() {} : function () { $('#contactlist').trigger('shown');};
|
||||
toggleContactList () {
|
||||
var completeFunction = ContactList.isVisible()
|
||||
? function () {}
|
||||
: function () {
|
||||
$('#contactlist').trigger('shown');
|
||||
};
|
||||
VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
|
||||
|
||||
toggle(ContactList,
|
||||
|
@ -131,12 +137,12 @@ var PanelToggler = (function(my) {
|
|||
ContactList.setVisualNotification(false);
|
||||
},
|
||||
null);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens / closes the settings menu
|
||||
*/
|
||||
my.toggleSettingsMenu = function() {
|
||||
toggleSettingsMenu () {
|
||||
VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){});
|
||||
toggle(SettingsMenu,
|
||||
'#settingsmenu',
|
||||
|
@ -147,31 +153,13 @@ var PanelToggler = (function(my) {
|
|||
$('#setEmail').get(0).value = settings.email;
|
||||
},
|
||||
null);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
isVisible () {
|
||||
return (Chat.isVisible() ||
|
||||
ContactList.isVisible() ||
|
||||
SettingsMenu.isVisible());
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return my;
|
||||
|
||||
}(PanelToggler || {}));
|
||||
|
||||
module.exports = PanelToggler;
|
||||
export default PanelToggler;
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/* global APP, $, Util, nickname:true */
|
||||
var Replacement = require("./Replacement");
|
||||
var CommandsProcessor = require("./Commands");
|
||||
var ToolbarToggler = require("../../toolbars/ToolbarToggler");
|
||||
/* global APP, $ */
|
||||
|
||||
import {processReplacements, linkify} from './Replacement';
|
||||
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 NicknameHandler = require("../../util/NicknameHandler");
|
||||
var UIUtil = require("../../util/UIUtil");
|
||||
var UIEvents = require("../../../../service/UI/UIEvents");
|
||||
|
||||
var notificationInterval = false;
|
||||
var unreadMessages = 0;
|
||||
|
@ -165,28 +167,21 @@ function resizeChatConversation() {
|
|||
/**
|
||||
* Chat related user interface.
|
||||
*/
|
||||
var Chat = (function (my) {
|
||||
var Chat = {
|
||||
/**
|
||||
* Initializes chat related interface.
|
||||
*/
|
||||
my.init = function () {
|
||||
if(NicknameHandler.getNickname())
|
||||
init (eventEmitter) {
|
||||
if (APP.settings.getDisplayName()) {
|
||||
Chat.setChatConversationMode(true);
|
||||
NicknameHandler.addListener(UIEvents.NICKNAME_CHANGED,
|
||||
function (nickname) {
|
||||
Chat.setChatConversationMode(true);
|
||||
});
|
||||
}
|
||||
|
||||
$('#nickinput').keydown(function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
var val = UIUtil.escapeHtml(this.value);
|
||||
this.value = '';
|
||||
if (!NicknameHandler.getNickname()) {
|
||||
NicknameHandler.setNickname(val);
|
||||
|
||||
return;
|
||||
}
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -197,14 +192,12 @@ var Chat = (function (my) {
|
|||
var value = this.value;
|
||||
usermsg.val('').trigger('autosize.resize');
|
||||
this.focus();
|
||||
var command = new CommandsProcessor(value);
|
||||
if(command.isCommand()) {
|
||||
var command = new CommandsProcessor(value, eventEmitter);
|
||||
if (command.isCommand()) {
|
||||
command.processCommand();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
var message = UIUtil.escapeHtml(value);
|
||||
APP.xmpp.sendChatMessage(message,
|
||||
NicknameHandler.getNickname());
|
||||
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -222,19 +215,17 @@ var Chat = (function (my) {
|
|||
});
|
||||
|
||||
addSmileys();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends the given message to the chat conversation.
|
||||
*/
|
||||
my.updateChatConversation =
|
||||
function (from, displayName, message, myjid, stamp) {
|
||||
updateChatConversation (id, displayName, message, stamp) {
|
||||
var divClassName = '';
|
||||
|
||||
if (APP.xmpp.myJid() === from) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
divClassName = "localuser";
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
divClassName = "remoteuser";
|
||||
|
||||
if (!Chat.isVisible()) {
|
||||
|
@ -250,7 +241,7 @@ var Chat = (function (my) {
|
|||
var escMessage = message.replace(/</g, '<').
|
||||
replace(/>/g, '>').replace(/\n/g, '<br/>');
|
||||
var escDisplayName = UIUtil.escapeHtml(displayName);
|
||||
message = Replacement.processReplacements(escMessage);
|
||||
message = processReplacements(escMessage);
|
||||
|
||||
var messageContainer =
|
||||
'<div class="chatmessage">'+
|
||||
|
@ -263,14 +254,14 @@ var Chat = (function (my) {
|
|||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends error message to the conversation
|
||||
* @param errorMessage the received error message.
|
||||
* @param originalText the original message.
|
||||
*/
|
||||
my.chatAddError = function(errorMessage, originalText) {
|
||||
chatAddError (errorMessage, originalText) {
|
||||
errorMessage = UIUtil.escapeHtml(errorMessage);
|
||||
originalText = UIUtil.escapeHtml(originalText);
|
||||
|
||||
|
@ -281,28 +272,28 @@ var Chat = (function (my) {
|
|||
(errorMessage? (' Reason: ' + errorMessage) : '') + '</div>');
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the subject to the UI
|
||||
* @param subject the subject
|
||||
*/
|
||||
my.chatSetSubject = function(subject) {
|
||||
if (subject)
|
||||
setSubject (subject) {
|
||||
if (subject) {
|
||||
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"});
|
||||
}
|
||||
else {
|
||||
$("#subject").css({display: "block"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the chat conversation mode.
|
||||
*/
|
||||
my.setChatConversationMode = function (isConversationMode) {
|
||||
setChatConversationMode (isConversationMode) {
|
||||
if (isConversationMode) {
|
||||
$('#nickname').css({visibility: 'hidden'});
|
||||
$('#chatconversation').css({visibility: 'visible'});
|
||||
|
@ -310,42 +301,37 @@ var Chat = (function (my) {
|
|||
$('#smileysarea').css({visibility: 'visible'});
|
||||
$('#usermsg').focus();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the chat area.
|
||||
*/
|
||||
my.resizeChat = function () {
|
||||
var chatSize = require("../SidePanelToggler").getPanelSize();
|
||||
|
||||
$('#chatspace').width(chatSize[0]);
|
||||
$('#chatspace').height(chatSize[1]);
|
||||
resizeChat (width, height) {
|
||||
$('#chatspace').width(width).height(height);
|
||||
|
||||
resizeChatConversation();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the chat is currently visible.
|
||||
*/
|
||||
my.isVisible = function () {
|
||||
isVisible () {
|
||||
return $('#chatspace').is(":visible");
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Shows and hides the window with the smileys
|
||||
*/
|
||||
my.toggleSmileys = toggleSmileys;
|
||||
toggleSmileys,
|
||||
|
||||
/**
|
||||
* Scrolls chat to the bottom.
|
||||
*/
|
||||
my.scrollChatToBottom = function() {
|
||||
scrollChatToBottom () {
|
||||
setTimeout(function () {
|
||||
$('#chatconversation').scrollTop(
|
||||
$('#chatconversation')[0].scrollHeight);
|
||||
}, 5);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return my;
|
||||
}(Chat || {}));
|
||||
module.exports = Chat;
|
||||
export default Chat;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
/* global APP, require */
|
||||
var UIUtil = require("../../util/UIUtil");
|
||||
/* global APP */
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
/**
|
||||
* List with supported commands. The keys are the names of the commands and
|
||||
* the value is the function that processes the message.
|
||||
* @type {{String: function}}
|
||||
*/
|
||||
var commands = {
|
||||
const commands = {
|
||||
"topic" : processTopic
|
||||
};
|
||||
|
||||
|
@ -29,9 +30,9 @@ function getCommand(message) {
|
|||
* Processes the data for topic command.
|
||||
* @param commandArguments the arguments of the topic command.
|
||||
*/
|
||||
function processTopic(commandArguments) {
|
||||
function processTopic(commandArguments, emitter) {
|
||||
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
|
||||
* @constructor
|
||||
*/
|
||||
function CommandsProcessor(message) {
|
||||
function CommandsProcessor(message, emitter) {
|
||||
var command = getCommand(message);
|
||||
|
||||
this.emitter = emitter;
|
||||
|
||||
/**
|
||||
* Returns the name of the command.
|
||||
* @returns {String} the command
|
||||
|
@ -80,7 +83,7 @@ CommandsProcessor.prototype.processCommand = function() {
|
|||
if(!this.isCommand())
|
||||
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 */
|
||||
var Smileys = require("./smileys.json");
|
||||
|
||||
/**
|
||||
* Processes links and smileys in "body"
|
||||
*/
|
||||
function processReplacements(body)
|
||||
{
|
||||
export function processReplacements(body) {
|
||||
//make links clickable
|
||||
body = linkify(body);
|
||||
|
||||
|
@ -18,8 +18,7 @@ function processReplacements(body)
|
|||
* Finds and replaces all links in the links in "body"
|
||||
* with their <a href=""></a>
|
||||
*/
|
||||
function linkify(inputText)
|
||||
{
|
||||
export function linkify(inputText) {
|
||||
var replacedText, replacePattern1, replacePattern2, replacePattern3;
|
||||
|
||||
//URLs starting with http://, https://, or ftp://
|
||||
|
@ -40,8 +39,7 @@ function linkify(inputText)
|
|||
/**
|
||||
* Replaces common smiley strings with images
|
||||
*/
|
||||
function smilify(body)
|
||||
{
|
||||
function smilify(body) {
|
||||
if(!body) {
|
||||
return body;
|
||||
}
|
||||
|
@ -56,8 +54,3 @@ function smilify(body)
|
|||
|
||||
return body;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processReplacements: processReplacements,
|
||||
linkify: linkify
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/* global $, APP, Strophe */
|
||||
var Avatar = require('../../avatar/Avatar');
|
||||
/* global $, APP */
|
||||
import Avatar from '../../avatar/Avatar';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
var numberOfContacts = 0;
|
||||
var notificationInterval;
|
||||
let numberOfContacts = 0;
|
||||
let notificationInterval;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function createAvatar(jid) {
|
||||
var avatar = document.createElement('img');
|
||||
let avatar = document.createElement('img');
|
||||
avatar.className = "icon-avatar avatar";
|
||||
avatar.src = Avatar.getAvatarUrl(jid);
|
||||
|
||||
|
@ -43,12 +44,12 @@ function createAvatar(jid) {
|
|||
* @param displayName the display name to set
|
||||
*/
|
||||
function createDisplayNameParagraph(key, displayName) {
|
||||
var p = document.createElement('p');
|
||||
if(displayName)
|
||||
p.innerText = displayName;
|
||||
else if(key) {
|
||||
let p = document.createElement('p');
|
||||
if (displayName) {
|
||||
p.innerHTML = displayName;
|
||||
} else if(key) {
|
||||
p.setAttribute("data-i18n",key);
|
||||
p.innerText = APP.translation.translateString(key);
|
||||
p.innerHTML = APP.translation.translateString(key);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
var ContactList = {
|
||||
init (emitter) {
|
||||
this.emitter = emitter;
|
||||
},
|
||||
/**
|
||||
* Indicates if the chat is currently visible.
|
||||
*
|
||||
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
|
||||
* otherwise
|
||||
*/
|
||||
isVisible: function () {
|
||||
isVisible () {
|
||||
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) {
|
||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
||||
addContact (id) {
|
||||
let contactlist = $('#contacts');
|
||||
|
||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
||||
|
||||
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;
|
||||
let newContact = document.createElement('li');
|
||||
newContact.id = id;
|
||||
newContact.className = "clickable";
|
||||
newContact.onclick = function (event) {
|
||||
newContact.onclick = (event) => {
|
||||
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"));
|
||||
|
||||
if (resourceJid === APP.xmpp.myResource()) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
contactlist.prepend(newContact);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
contactlist.append(newContact);
|
||||
}
|
||||
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) {
|
||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
||||
|
||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
||||
|
||||
if (contact && contact.length > 0) {
|
||||
var contactlist = $('#contactlist>ul');
|
||||
|
||||
contactlist.get(0).removeChild(contact.get(0));
|
||||
removeContact (id) {
|
||||
let contact = getContactEl(id);
|
||||
|
||||
if (contact.length > 0) {
|
||||
contact.remove();
|
||||
updateNumberOfParticipants(-1);
|
||||
}
|
||||
},
|
||||
|
||||
setVisualNotification: function (show, stopGlowingIn) {
|
||||
var glower = $('#contactListButton');
|
||||
setVisualNotification (show, stopGlowingIn) {
|
||||
let glower = $('#contactListButton');
|
||||
|
||||
if (show && !notificationInterval) {
|
||||
notificationInterval = window.setInterval(function () {
|
||||
glower.toggleClass('active glowing');
|
||||
}, 800);
|
||||
}
|
||||
else if (!show && notificationInterval) {
|
||||
} else if (!show && notificationInterval) {
|
||||
stopGlowing(glower);
|
||||
}
|
||||
if (stopGlowingIn) {
|
||||
|
@ -160,35 +147,28 @@ var ContactList = {
|
|||
}
|
||||
},
|
||||
|
||||
setClickable: function (resourceJid, isClickable) {
|
||||
var contact = $('#contacts>li[id="' + resourceJid + '"]');
|
||||
if (isClickable) {
|
||||
contact.addClass('clickable');
|
||||
} else {
|
||||
contact.removeClass('clickable');
|
||||
}
|
||||
setClickable (id, isClickable) {
|
||||
getContactEl(id).toggleClass('clickable', isClickable);
|
||||
},
|
||||
|
||||
onDisplayNameChange: function (peerJid, displayName) {
|
||||
if (peerJid === 'localVideoContainer')
|
||||
peerJid = APP.xmpp.myJid();
|
||||
onDisplayNameChange (id, displayName) {
|
||||
if (id === 'localVideoContainer') {
|
||||
id = APP.conference.localId;
|
||||
}
|
||||
let contactName = $(`#contacts #${id}>p`);
|
||||
|
||||
var resourceJid = Strophe.getResourceFromJid(peerJid);
|
||||
|
||||
var contactName = $('#contacts #' + resourceJid + '>p');
|
||||
|
||||
if (contactName && displayName && displayName.length > 0)
|
||||
if (displayName) {
|
||||
contactName.html(displayName);
|
||||
}
|
||||
},
|
||||
|
||||
userAvatarChanged: function (resourceJid, avatarUrl) {
|
||||
changeUserAvatar (id, avatarUrl) {
|
||||
// set the avatar in the contact list
|
||||
var contact = $('#' + resourceJid + '>img');
|
||||
if (contact && contact.length > 0) {
|
||||
contact.get(0).src = avatarUrl;
|
||||
let contact = $(`#${id}>img`);
|
||||
if (contact.length > 0) {
|
||||
contact.attr('src', avatarUrl);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ContactList;
|
||||
export default ContactList;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* global APP, $ */
|
||||
var Avatar = require("../../avatar/Avatar");
|
||||
var Settings = require("./../../../settings/Settings");
|
||||
var UIUtil = require("../../util/UIUtil");
|
||||
var languages = require("../../../../service/translation/languages");
|
||||
import UIUtil from "../../util/UIUtil";
|
||||
import UIEvents from "../../../../service/UI/UIEvents";
|
||||
import languages from "../../../../service/translation/languages";
|
||||
import Settings from '../../../settings/Settings';
|
||||
|
||||
function generateLanguagesSelectBox() {
|
||||
var currentLang = APP.translation.getCurrentLanguage();
|
||||
var html = "<select id=\"languages_selectbox\">";
|
||||
var html = '<select id="languages_selectbox">';
|
||||
var langArray = languages.getLanguages();
|
||||
for(var i = 0; i < langArray.length; 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 () {
|
||||
var startMutedSelector = $("#startMutedOptions");
|
||||
startMutedSelector.before(generateLanguagesSelectBox());
|
||||
APP.translation.translateElement($("#languages_selectbox"));
|
||||
$('#settingsmenu>input').keyup(function(event){
|
||||
if(event.keyCode === 13) {//enter
|
||||
SettingsMenu.update();
|
||||
if (displayName && Settings.getDisplayName() !== displayName) {
|
||||
emitter.emit(UIEvents.NICKNAME_CHANGED, displayName);
|
||||
}
|
||||
});
|
||||
|
||||
if (APP.xmpp.isModerator()) {
|
||||
startMutedSelector.css("display", "block");
|
||||
}
|
||||
else {
|
||||
startMutedSelector.css("display", "none");
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$("#updateSettings").click(function () {
|
||||
SettingsMenu.update();
|
||||
let startMutedBlock = $("#startMutedOptions");
|
||||
startMutedBlock.before(generateLanguagesSelectBox());
|
||||
APP.translation.translateElement($("#languages_selectbox"));
|
||||
|
||||
this.onRoleChanged();
|
||||
this.onStartMutedChanged();
|
||||
|
||||
$("#updateSettings").click(update);
|
||||
$('#settingsmenu>input').keyup(function(event){
|
||||
if (event.keyCode === 13) {//enter
|
||||
update();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onRoleChanged: function () {
|
||||
if(APP.xmpp.isModerator()) {
|
||||
onRoleChanged () {
|
||||
if(APP.conference.isModerator) {
|
||||
$("#startMutedOptions").css("display", "block");
|
||||
}
|
||||
else {
|
||||
|
@ -55,55 +77,22 @@ var SettingsMenu = {
|
|||
}
|
||||
},
|
||||
|
||||
setStartMuted: function (audio, video) {
|
||||
$("#startAudioMuted").attr("checked", audio);
|
||||
$("#startVideoMuted").attr("checked", video);
|
||||
onStartMutedChanged () {
|
||||
$("#startAudioMuted").attr("checked", APP.conference.startAudioMuted);
|
||||
$("#startVideoMuted").attr("checked", APP.conference.startVideoMuted);
|
||||
},
|
||||
|
||||
update: function() {
|
||||
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() {
|
||||
isVisible () {
|
||||
return $('#settingsmenu').is(':visible');
|
||||
},
|
||||
|
||||
setDisplayName: function(newDisplayName) {
|
||||
var displayName = Settings.setDisplayName(newDisplayName);
|
||||
$('#setDisplayName').get(0).value = displayName;
|
||||
},
|
||||
|
||||
onDisplayNameChange: function(peerJid, newDisplayName) {
|
||||
if(peerJid === 'localVideoContainer' ||
|
||||
peerJid === APP.xmpp.myJid()) {
|
||||
this.setDisplayName(newDisplayName);
|
||||
onDisplayNameChange (id, newDisplayName) {
|
||||
if(id === 'localVideoContainer' || APP.conference.isLocalId(id)) {
|
||||
$('#setDisplayName').val(newDisplayName);
|
||||
}
|
||||
},
|
||||
changeAvatar: function (thumbUrl) {
|
||||
$('#avatar').get(0).src = thumbUrl;
|
||||
|
||||
changeAvatar (avatarUrl) {
|
||||
$('#avatar').attr('src', avatarUrl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = SettingsMenu;
|
|
@ -1,66 +1,109 @@
|
|||
/* global $ */
|
||||
var PanelToggler = require("../side_pannels/SidePanelToggler");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter");
|
||||
var UIEvents = require("../../../service/UI/UIEvents");
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
|
||||
|
||||
var eventEmitter = null;
|
||||
|
||||
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',
|
||||
'contacts': '#bottom_toolbar_contact_list',
|
||||
const defaultBottomToolbarButtons = {
|
||||
'chat': '#bottom_toolbar_chat',
|
||||
'contacts': '#bottom_toolbar_contact_list',
|
||||
'filmstrip': '#bottom_toolbar_film_strip'
|
||||
};
|
||||
|
||||
const BottomToolbar = {
|
||||
init () {
|
||||
this.filmStrip = $('#remoteVideos');
|
||||
this.toolbar = $('#bottomToolbar');
|
||||
},
|
||||
|
||||
var BottomToolbar = (function (my) {
|
||||
my.init = function (emitter) {
|
||||
eventEmitter = emitter;
|
||||
setupListeners (emitter) {
|
||||
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
|
||||
|
||||
for(var k in buttonHandlers)
|
||||
$("#" + k).click(buttonHandlers[k]);
|
||||
};
|
||||
const buttonHandlers = {
|
||||
"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() {
|
||||
PanelToggler.toggleChat();
|
||||
};
|
||||
Object.keys(buttonHandlers).forEach(
|
||||
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
|
||||
);
|
||||
},
|
||||
|
||||
my.toggleContactList = function() {
|
||||
PanelToggler.toggleContactList();
|
||||
};
|
||||
toggleFilmStrip () {
|
||||
this.filmStrip.toggleClass("hidden");
|
||||
},
|
||||
|
||||
my.toggleFilmStrip = function() {
|
||||
var filmstrip = $("#remoteVideos");
|
||||
filmstrip.toggleClass("hidden");
|
||||
isFilmStripVisible () {
|
||||
return !this.filmStrip.hasClass('hidden');
|
||||
},
|
||||
|
||||
eventEmitter.emit( UIEvents.FILM_STRIP_TOGGLED,
|
||||
filmstrip.hasClass("hidden"));
|
||||
};
|
||||
setupFilmStripOnly () {
|
||||
this.filmStrip.css({
|
||||
padding: "0px 0px 18px 0px",
|
||||
right: 0
|
||||
});
|
||||
},
|
||||
|
||||
$(document).bind("remotevideo.resized", function (event, width, height) {
|
||||
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
|
||||
getFilmStripHeight () {
|
||||
if (this.isFilmStripVisible()) {
|
||||
return this.filmStrip.outerHeight();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
$('#bottomToolbar').css({bottom: bottom + 'px'});
|
||||
});
|
||||
getFilmStripWidth () {
|
||||
return this.filmStrip.width();
|
||||
},
|
||||
|
||||
return my;
|
||||
}(BottomToolbar || {}));
|
||||
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
|
||||
});
|
||||
|
||||
module.exports = BottomToolbar;
|
||||
this.getThumbs(!forceUpdate).animate({
|
||||
height: thumbHeight,
|
||||
width: thumbWidth
|
||||
}, {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0,
|
||||
complete: resolve
|
||||
});
|
||||
|
||||
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,
|
||||
Util */
|
||||
/* global APP, $, config, interfaceConfig */
|
||||
/* jshint -W101 */
|
||||
var messageHandler = require("../util/MessageHandler");
|
||||
var BottomToolbar = require("./BottomToolbar");
|
||||
var Prezi = require("../prezi/Prezi");
|
||||
var Etherpad = require("../etherpad/Etherpad");
|
||||
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");
|
||||
import messageHandler from '../util/MessageHandler';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
var roomUrl = null;
|
||||
var sharedKey = '';
|
||||
var UI = null;
|
||||
var recordingToaster = null;
|
||||
let roomUrl = null;
|
||||
let recordingToaster = null;
|
||||
let emitter = 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 () {
|
||||
if (APP.RTC.localAudio.isMuted()) {
|
||||
if (APP.conference.audioMuted) {
|
||||
AnalyticsAdapter.sendEvent('toolbar.audio.unmuted');
|
||||
emitter.emit(UIEvents.AUDIO_MUTED, false);
|
||||
} else {
|
||||
AnalyticsAdapter.sendEvent('toolbar.audio.muted');
|
||||
emitter.emit(UIEvents.AUDIO_MUTED, true);
|
||||
}
|
||||
return APP.UI.toggleAudio();
|
||||
},
|
||||
"toolbar_button_camera": function () {
|
||||
if (APP.RTC.localVideo.isMuted()) {
|
||||
if (APP.conference.videoMuted) {
|
||||
AnalyticsAdapter.sendEvent('toolbar.video.enabled');
|
||||
emitter.emit(UIEvents.VIDEO_MUTED, false);
|
||||
} else {
|
||||
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 () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.recording.toggled');
|
||||
return toggleRecording();
|
||||
emitter.emit(UIEvents.RECORDING_TOGGLE);
|
||||
},
|
||||
"toolbar_button_security": function () {
|
||||
if (sharedKey) {
|
||||
AnalyticsAdapter.sendEvent('toolbar.lock.disabled');
|
||||
} else {
|
||||
AnalyticsAdapter.sendEvent('toolbar.lock.enabled');
|
||||
}
|
||||
return Toolbar.openLockDialog();
|
||||
emitter.emit(UIEvents.ROOM_LOCK_CLICKED);
|
||||
},
|
||||
"toolbar_button_link": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.invite.clicked');
|
||||
return Toolbar.openLinkDialog();
|
||||
openLinkDialog();
|
||||
},
|
||||
"toolbar_button_chat": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.chat.toggled');
|
||||
return BottomToolbar.toggleChat();
|
||||
emitter.emit(UIEvents.TOGGLE_CHAT);
|
||||
},
|
||||
"toolbar_button_prezi": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.prezi.clicked');
|
||||
return Prezi.openPreziDialog();
|
||||
emitter.emit(UIEvents.PREZI_CLICKED);
|
||||
},
|
||||
"toolbar_button_etherpad": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
|
||||
return Etherpad.toggleEtherpad(0);
|
||||
emitter.emit(UIEvents.ETHERPAD_CLICKED);
|
||||
},
|
||||
"toolbar_button_desktopsharing": function () {
|
||||
if (APP.desktopsharing.isUsingScreenStream) {
|
||||
|
@ -72,32 +136,32 @@ var buttonHandlers = {
|
|||
} else {
|
||||
AnalyticsAdapter.sendEvent('toolbar.screen.enabled');
|
||||
}
|
||||
return APP.desktopsharing.toggleScreenSharing();
|
||||
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||
},
|
||||
"toolbar_button_fullScreen": function() {
|
||||
AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled');
|
||||
UIUtil.buttonClick("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
|
||||
return Toolbar.toggleFullScreen();
|
||||
emitter.emit(UIEvents.FULLSCREEN_TOGGLE);
|
||||
},
|
||||
"toolbar_button_sip": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.sip.clicked');
|
||||
return callSipButtonClicked();
|
||||
showSipNumberInput();
|
||||
},
|
||||
"toolbar_button_dialpad": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.sip.dialpad.clicked');
|
||||
return dialpadButtonClicked();
|
||||
dialpadButtonClicked();
|
||||
},
|
||||
"toolbar_button_settings": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.settings.toggled');
|
||||
PanelToggler.toggleSettingsMenu();
|
||||
emitter.emit(UIEvents.TOGGLE_SETTINGS);
|
||||
},
|
||||
"toolbar_button_hangup": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.hangup');
|
||||
return hangup();
|
||||
emitter.emit(UIEvents.HANGUP);
|
||||
},
|
||||
"toolbar_button_login": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.authenticate.login.clicked');
|
||||
Toolbar.authenticateClicked();
|
||||
emitter.emit(UIEvents.AUTH_CLICKED);
|
||||
},
|
||||
"toolbar_button_logout": function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.authenticate.logout.clicked');
|
||||
|
@ -111,631 +175,218 @@ var buttonHandlers = {
|
|||
"dialog.Yes",
|
||||
function (evt, yes) {
|
||||
if (yes) {
|
||||
APP.xmpp.logout(function (url) {
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
hangup();
|
||||
}
|
||||
});
|
||||
emitter.emit(UIEvents.LOGOUT);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
var defaultToolbarButtons = {
|
||||
'microphone': '#toolbar_button_mute',
|
||||
'camera': '#toolbar_button_camera',
|
||||
'desktop': '#toolbar_button_desktopsharing',
|
||||
'security': '#toolbar_button_security',
|
||||
'invite': '#toolbar_button_link',
|
||||
'chat': '#toolbar_button_chat',
|
||||
'prezi': '#toolbar_button_prezi',
|
||||
'etherpad': '#toolbar_button_etherpad',
|
||||
'fullscreen': '#toolbar_button_fullScreen',
|
||||
'settings': '#toolbar_button_settings',
|
||||
'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');
|
||||
}
|
||||
};
|
||||
const defaultToolbarButtons = {
|
||||
'microphone': '#toolbar_button_mute',
|
||||
'camera': '#toolbar_button_camera',
|
||||
'desktop': '#toolbar_button_desktopsharing',
|
||||
'security': '#toolbar_button_security',
|
||||
'invite': '#toolbar_button_link',
|
||||
'chat': '#toolbar_button_chat',
|
||||
'prezi': '#toolbar_button_prezi',
|
||||
'etherpad': '#toolbar_button_etherpad',
|
||||
'fullscreen': '#toolbar_button_fullScreen',
|
||||
'settings': '#toolbar_button_settings',
|
||||
'hangup': '#toolbar_button_hangup'
|
||||
};
|
||||
|
||||
function dialpadButtonClicked() {
|
||||
//TODO show the dialpad box
|
||||
}
|
||||
|
||||
function callSipButtonClicked() {
|
||||
var defaultNumber
|
||||
= config.defaultSipNumber ? config.defaultSipNumber : '';
|
||||
function showSipNumberInput () {
|
||||
let defaultNumber = config.defaultSipNumber
|
||||
? config.defaultSipNumber
|
||||
: '';
|
||||
|
||||
var sipMsg = APP.translation.generateTranslationHTML(
|
||||
"dialog.sipMsg");
|
||||
messageHandler.openTwoButtonDialog(null, null, null,
|
||||
'<h2>' + sipMsg + '</h2>' +
|
||||
'<input name="sipNumber" type="text"' +
|
||||
' value="' + defaultNumber + '" autofocus>',
|
||||
false,
|
||||
"dialog.Dial",
|
||||
let sipMsg = APP.translation.generateTranslationHTML("dialog.sipMsg");
|
||||
messageHandler.openTwoButtonDialog(
|
||||
null, null, null,
|
||||
`<h2>${sipMsg}</h2>
|
||||
<input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
|
||||
false, "dialog.Dial",
|
||||
function (e, v, m, f) {
|
||||
if (v) {
|
||||
var numberInput = f.sipNumber;
|
||||
if (numberInput) {
|
||||
APP.xmpp.dial(
|
||||
numberInput, 'fromnumber', UI.getRoomName(), sharedKey);
|
||||
}
|
||||
if (v && f.sipNumber) {
|
||||
emitter.emit(UIEvents.SIP_DIAL, f.sipNumber);
|
||||
}
|
||||
},
|
||||
null, null, ':input:first'
|
||||
);
|
||||
}
|
||||
|
||||
var Toolbar = (function (my) {
|
||||
const Toolbar = {
|
||||
init (eventEmitter) {
|
||||
emitter = eventEmitter;
|
||||
|
||||
my.init = function (ui) {
|
||||
UIUtil.hideDisabledButtons(defaultToolbarButtons);
|
||||
|
||||
for(var k in buttonHandlers)
|
||||
$("#" + k).click(buttonHandlers[k]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
Object.keys(buttonHandlers).forEach(
|
||||
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
my.updateRoomUrl = function (newRoomUrl) {
|
||||
updateRoomUrl (newRoomUrl) {
|
||||
roomUrl = newRoomUrl;
|
||||
|
||||
// If the invite dialog has been already opened we update the information.
|
||||
var inviteLink = document.getElementById('inviteLinkRef');
|
||||
let inviteLink = document.getElementById('inviteLinkRef');
|
||||
if (inviteLink) {
|
||||
inviteLink.value = roomUrl;
|
||||
inviteLink.select();
|
||||
$('#inviteLinkRef').parent()
|
||||
.find('button[value=true]').prop('disabled', false);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables and enables some of the buttons.
|
||||
*/
|
||||
my.setupButtonsFromConfig = function () {
|
||||
setupButtonsFromConfig () {
|
||||
if (!UIUtil.isButtonEnabled('prezi')) {
|
||||
$("#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.
|
||||
*/
|
||||
my.unlockLockButton = function () {
|
||||
unlockLockButton () {
|
||||
if ($("#toolbar_button_security").hasClass("icon-security-locked"))
|
||||
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the lock button state to locked.
|
||||
*/
|
||||
my.lockLockButton = function () {
|
||||
lockLockButton () {
|
||||
if ($("#toolbar_button_security").hasClass("icon-security"))
|
||||
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows or hides authentication button
|
||||
* @param show <tt>true</tt> to show or <tt>false</tt> to hide
|
||||
*/
|
||||
my.showAuthenticateButton = function (show) {
|
||||
showAuthenticateButton (show) {
|
||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||
$('#authentication').css({display: "inline"});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$('#authentication').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
showEtherpadButton () {
|
||||
if (!$('#toolbar_button_etherpad').is(":visible")) {
|
||||
$('#toolbar_button_etherpad').css({display: 'inline-block'});
|
||||
}
|
||||
},
|
||||
|
||||
// Shows or hides the 'recording' button.
|
||||
my.showRecordingButton = function (show) {
|
||||
showRecordingButton (show) {
|
||||
if (UIUtil.isButtonEnabled('recording') && show) {
|
||||
$('#toolbar_button_record').css({display: "inline-block"});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$('#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
|
||||
// to start automatically recording
|
||||
my.checkAutoRecord = function () {
|
||||
checkAutoRecord () {
|
||||
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
|
||||
// we have params to start automatically sharing
|
||||
my.checkAutoEnableDesktopSharing = function () {
|
||||
if (UIUtil.isButtonEnabled('desktop')
|
||||
&& config.autoEnableDesktopSharing) {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
checkAutoEnableDesktopSharing () {
|
||||
if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) {
|
||||
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Shows or hides SIP calls button
|
||||
my.showSipCallButton = function (show) {
|
||||
if (APP.xmpp.isSipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
|
||||
showSipCallButton (show) {
|
||||
if (APP.conference.sipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
|
||||
$('#toolbar_button_sip').css({display: "inline-block"});
|
||||
} else {
|
||||
$('#toolbar_button_sip').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Shows or hides the dialpad button
|
||||
my.showDialPadButton = function (show) {
|
||||
showDialPadButton (show) {
|
||||
if (UIUtil.isButtonEnabled('dialpad') && show) {
|
||||
$('#toolbar_button_dialpad').css({display: "inline-block"});
|
||||
} else {
|
||||
$('#toolbar_button_dialpad').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays user authenticated identity name(login).
|
||||
* @param authIdentity identity name to be displayed.
|
||||
*/
|
||||
my.setAuthenticatedIdentity = function (authIdentity) {
|
||||
setAuthenticatedIdentity (authIdentity) {
|
||||
if (authIdentity) {
|
||||
var selector = $('#toolbar_auth_identity');
|
||||
let selector = $('#toolbar_auth_identity');
|
||||
selector.css({display: "list-item"});
|
||||
selector.text(authIdentity);
|
||||
} else {
|
||||
$('#toolbar_auth_identity').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides login button.
|
||||
* @param show <tt>true</tt> to show
|
||||
*/
|
||||
my.showLoginButton = function (show) {
|
||||
showLoginButton (show) {
|
||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||
$('#toolbar_button_login').css({display: "list-item"});
|
||||
} else {
|
||||
$('#toolbar_button_login').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides logout button.
|
||||
* @param show <tt>true</tt> to show
|
||||
*/
|
||||
my.showLogoutButton = function (show) {
|
||||
showLogoutButton (show) {
|
||||
if (UIUtil.isButtonEnabled('authentication') && show) {
|
||||
$('#toolbar_button_logout').css({display: "list-item"});
|
||||
} else {
|
||||
$('#toolbar_button_logout').css({display: "none"});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the state of the button. The button has blue glow if desktop
|
||||
* streaming is active.
|
||||
* @param active the state of the desktop streaming.
|
||||
*/
|
||||
my.changeDesktopSharingButtonState = function (active) {
|
||||
var button = $("#toolbar_button_desktopsharing");
|
||||
changeDesktopSharingButtonState (active) {
|
||||
let button = $("#toolbar_button_desktopsharing");
|
||||
if (active) {
|
||||
button.addClass("glow");
|
||||
} else {
|
||||
button.removeClass("glow");
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
return my;
|
||||
}(Toolbar || {}));
|
||||
updateRecordingState (state) {
|
||||
setRecordingButtonState(state);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Toolbar;
|
||||
export default Toolbar;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* global APP, config, $, interfaceConfig, Moderator,
|
||||
DesktopStreaming.showDesktopSharingButton */
|
||||
/* global APP, config, $, interfaceConfig */
|
||||
|
||||
var toolbarTimeoutObject,
|
||||
toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT,
|
||||
UIUtil = require("../util/UIUtil");
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import BottomToolbar from './BottomToolbar';
|
||||
|
||||
let toolbarTimeoutObject;
|
||||
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
|
||||
|
||||
function showDesktopSharingButton() {
|
||||
if (APP.desktopsharing.isDesktopSharingEnabled() &&
|
||||
|
@ -14,19 +15,24 @@ function showDesktopSharingButton() {
|
|||
}
|
||||
}
|
||||
|
||||
function isToolbarVisible () {
|
||||
return $('#header').is(':visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the toolbar.
|
||||
*/
|
||||
function hideToolbar() {
|
||||
if(config.alwaysVisibleToolbar)
|
||||
if (config.alwaysVisibleToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
var header = $("#header"),
|
||||
bottomToolbar = $("#bottomToolbar");
|
||||
var isToolbarHover = false;
|
||||
let header = $("#header");
|
||||
let bottomToolbar = $("#bottomToolbar");
|
||||
let isToolbarHover = false;
|
||||
header.find('*').each(function () {
|
||||
var id = $(this).attr('id');
|
||||
if ($("#" + id + ":hover").length > 0) {
|
||||
let id = $(this).attr('id');
|
||||
if ($(`#${id}:hover`).length > 0) {
|
||||
isToolbarHover = true;
|
||||
}
|
||||
});
|
||||
|
@ -37,34 +43,36 @@ function hideToolbar() {
|
|||
clearTimeout(toolbarTimeoutObject);
|
||||
toolbarTimeoutObject = null;
|
||||
|
||||
if (!isToolbarHover) {
|
||||
if (isToolbarHover) {
|
||||
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
||||
} else {
|
||||
header.hide("slide", { direction: "up", duration: 300});
|
||||
$('#subject').animate({top: "-=40"}, 300);
|
||||
if ($("#remoteVideos").hasClass("hidden")) {
|
||||
if (!BottomToolbar.isFilmStripVisible()) {
|
||||
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.
|
||||
*/
|
||||
showToolbar: function () {
|
||||
if (interfaceConfig.filmStripOnly)
|
||||
showToolbar () {
|
||||
if (interfaceConfig.filmStripOnly) {
|
||||
return;
|
||||
var header = $("#header"),
|
||||
bottomToolbar = $("#bottomToolbar");
|
||||
}
|
||||
let header = $("#header");
|
||||
let bottomToolbar = $("#bottomToolbar");
|
||||
if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
|
||||
header.show("slide", { direction: "up", duration: 300});
|
||||
$('#subject').animate({top: "+=40"}, 300);
|
||||
if (!bottomToolbar.is(":visible")) {
|
||||
bottomToolbar.show(
|
||||
"slide", {direction: "right", duration: 300});
|
||||
"slide", {direction: "right", duration: 300}
|
||||
);
|
||||
}
|
||||
|
||||
if (toolbarTimeoutObject) {
|
||||
|
@ -75,13 +83,6 @@ var ToolbarToggler = {
|
|||
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
|
||||
showDesktopSharingButton();
|
||||
},
|
||||
|
@ -91,33 +92,28 @@ var ToolbarToggler = {
|
|||
*
|
||||
* @param isDock indicates what operation to perform
|
||||
*/
|
||||
dockToolbar: function (isDock) {
|
||||
if (interfaceConfig.filmStripOnly)
|
||||
dockToolbar (isDock) {
|
||||
if (interfaceConfig.filmStripOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDock) {
|
||||
// First make sure the toolbar is shown.
|
||||
if (!$('#header').is(':visible')) {
|
||||
if (!isToolbarVisible()) {
|
||||
this.showToolbar();
|
||||
}
|
||||
|
||||
// Then clear the time out, to dock the toolbar.
|
||||
if (toolbarTimeoutObject) {
|
||||
clearTimeout(toolbarTimeoutObject);
|
||||
toolbarTimeoutObject = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$('#header').is(':visible')) {
|
||||
clearTimeout(toolbarTimeoutObject);
|
||||
toolbarTimeoutObject = null;
|
||||
} else {
|
||||
if (isToolbarVisible()) {
|
||||
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
|
||||
} else {
|
||||
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 */
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
getAvailableVideoWidth: function (isVisible) {
|
||||
var PanelToggler = require("../side_pannels/SidePanelToggler");
|
||||
if(typeof isVisible === "undefined" || isVisible === null)
|
||||
isVisible = PanelToggler.isVisible();
|
||||
var rightPanelWidth
|
||||
= isVisible ? PanelToggler.getPanelSize()[0] : 0;
|
||||
getAvailableVideoWidth: function (isSidePanelVisible) {
|
||||
let rightPanelWidth = 0;
|
||||
|
||||
if (isSidePanelVisible) {
|
||||
rightPanelWidth = UIUtil.getSidePanelSize()[0];
|
||||
}
|
||||
|
||||
return window.innerWidth - rightPanelWidth;
|
||||
},
|
||||
|
@ -112,5 +129,17 @@ var UIUtil = module.exports = {
|
|||
.filter(function (item) { return item; })
|
||||
.join(',');
|
||||
$(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, $ */
|
||||
/* jshint -W101 */
|
||||
var JitsiPopover = require("../util/JitsiPopover");
|
||||
import JitsiPopover from "../util/JitsiPopover";
|
||||
|
||||
/**
|
||||
* Constructs new connection indicator.
|
||||
* @param videoContainer the video container associated with the indicator.
|
||||
* @constructor
|
||||
*/
|
||||
function ConnectionIndicator(videoContainer, jid) {
|
||||
function ConnectionIndicator(videoContainer, id) {
|
||||
this.videoContainer = videoContainer;
|
||||
this.bandwidth = null;
|
||||
this.packetLoss = null;
|
||||
|
@ -16,7 +16,7 @@ function ConnectionIndicator(videoContainer, jid) {
|
|||
this.resolution = null;
|
||||
this.transport = [];
|
||||
this.popover = null;
|
||||
this.jid = jid;
|
||||
this.id = id;
|
||||
this.create();
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ ConnectionIndicator.prototype.generateText = function () {
|
|||
}
|
||||
|
||||
var resolutionValue = null;
|
||||
if(this.resolution && this.jid) {
|
||||
if(this.resolution && this.id) {
|
||||
var keys = Object.keys(this.resolution);
|
||||
for(var ssrc in this.resolution) {
|
||||
// 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 = "";
|
||||
if(this.resolution === null || !Object.keys(this.resolution) ||
|
||||
Object.keys(this.resolution).length === 0) {
|
||||
|
@ -144,8 +144,8 @@ ConnectionIndicator.prototype.generateText = function () {
|
|||
if(this.videoContainer.videoSpanId == "localVideoContainer") {
|
||||
result += "<div class=\"jitsipopover_showmore\" " +
|
||||
"onclick = \"APP.UI.connectionIndicatorShowMore('" +
|
||||
// FIXME: we do not know local jid when this text is generated
|
||||
//this.jid + "')\" data-i18n='connectionindicator." +
|
||||
// FIXME: we do not know local id when this text is generated
|
||||
//this.id + "')\" data-i18n='connectionindicator." +
|
||||
"local')\" data-i18n='connectionindicator." +
|
||||
(this.showMoreValue ? "less" : "more") + "'>" +
|
||||
translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
|
||||
|
@ -365,7 +365,8 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
|||
*/
|
||||
ConnectionIndicator.prototype.updatePopoverData = function () {
|
||||
this.popover.updateContent(
|
||||
"<div class=\"connection_info\">" + this.generateText() + "</div>");
|
||||
`<div class="connection_info">${this.generateText()}</div>`
|
||||
);
|
||||
APP.translation.translateElement($(".connection_info"));
|
||||
};
|
||||
|
||||
|
@ -385,4 +386,4 @@ ConnectionIndicator.prototype.hideIndicator = function () {
|
|||
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 */
|
||||
var SmallVideo = require("./SmallVideo");
|
||||
var ConnectionIndicator = require("./ConnectionIndicator");
|
||||
var NicknameHandler = require("../util/NicknameHandler");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
import ConnectionIndicator from "./ConnectionIndicator";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import SmallVideo from "./SmallVideo";
|
||||
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
|
||||
function LocalVideo(VideoLayout) {
|
||||
const TrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
function LocalVideo(VideoLayout, emitter) {
|
||||
this.videoSpanId = "localVideoContainer";
|
||||
this.container = $("#localVideoContainer").get(0);
|
||||
this.bindHoverHandler();
|
||||
this.VideoLayout = VideoLayout;
|
||||
this.flipX = true;
|
||||
this.isLocal = true;
|
||||
this.peerJid = null;
|
||||
this.emitter = emitter;
|
||||
SmallVideo.call(this);
|
||||
}
|
||||
|
||||
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
|
@ -61,6 +65,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
|
|||
$('#localDisplayName').html(defaultLocalDisplayName);
|
||||
}
|
||||
}
|
||||
this.updateView();
|
||||
} else {
|
||||
var editButton = createEditDisplayNameButton();
|
||||
|
||||
|
@ -117,12 +122,14 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
|
|||
|
||||
editDisplayName.one("focusout", function (e) {
|
||||
self.VideoLayout.inputDisplayNameHandler(this.value);
|
||||
$('#editDisplayName').hide();
|
||||
});
|
||||
|
||||
editDisplayName.on('keydown', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
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) {
|
||||
name = 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();
|
||||
this.emitter.emit(UIEvents.NICKNAME_CHANGED, UIUtil.escapeHtml(name));
|
||||
};
|
||||
|
||||
LocalVideo.prototype.createConnectionIndicator = function() {
|
||||
|
@ -158,37 +147,29 @@ LocalVideo.prototype.createConnectionIndicator = function() {
|
|||
this.connectionIndicator = new ConnectionIndicator(this, null);
|
||||
};
|
||||
|
||||
LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
||||
var self = this;
|
||||
LocalVideo.prototype.changeVideo = function (stream) {
|
||||
this.stream = stream;
|
||||
|
||||
function localVideoClick(event) {
|
||||
let localVideoClick = (event) => {
|
||||
// FIXME: with Temasys plugin event arg is not an event, but
|
||||
// the clicked object itself, so we have to skip this call
|
||||
if (event.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
self.VideoLayout.handleVideoThumbClicked(
|
||||
true,
|
||||
APP.xmpp.myResource());
|
||||
}
|
||||
this.VideoLayout.handleVideoThumbClicked(true, this.id);
|
||||
};
|
||||
|
||||
var localVideoContainerSelector = $('#localVideoContainer');
|
||||
let localVideoContainerSelector = $('#localVideoContainer');
|
||||
localVideoContainerSelector.off('click');
|
||||
localVideoContainerSelector.on('click', localVideoClick);
|
||||
|
||||
if(isMuted) {
|
||||
APP.UI.setVideoMute(true);
|
||||
return;
|
||||
}
|
||||
this.flipX = stream.videoType != "screen";
|
||||
var localVideo = document.createElement('video');
|
||||
localVideo.id = 'localVideo_' +
|
||||
APP.RTC.getStreamID(stream.getOriginalStream());
|
||||
this.flipX = stream.videoType != "desktop";
|
||||
let localVideo = document.createElement('video');
|
||||
localVideo.id = 'localVideo_' + stream.getId();
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
localVideo.autoplay = true;
|
||||
localVideo.volume = 0; // is it required if audio is separated ?
|
||||
}
|
||||
localVideo.oncontextmenu = function () { return false; };
|
||||
|
||||
var localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
// Put the new video always in front
|
||||
|
@ -207,29 +188,19 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
|||
}
|
||||
|
||||
// Attach WebRTC stream
|
||||
APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream());
|
||||
stream.attach(localVideoSelector);
|
||||
|
||||
// Add stream ended handler
|
||||
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>
|
||||
let endedHandler = () => {
|
||||
localVideo = $('#' + localVideo.id)[0];
|
||||
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) {
|
||||
this.peerJid = jid;
|
||||
LocalVideo.prototype.joined = function (id) {
|
||||
this.id = id;
|
||||
};
|
||||
|
||||
LocalVideo.prototype.getResourceJid = function () {
|
||||
var myResource = APP.xmpp.myResource();
|
||||
if (!myResource) {
|
||||
console.error("Requested local resource before we're in the MUC");
|
||||
}
|
||||
return myResource;
|
||||
};
|
||||
|
||||
module.exports = LocalVideo;
|
||||
export default LocalVideo;
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
/* global $, APP, require, Strophe, 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");
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
function RemoteVideo(peerJid, VideoLayout) {
|
||||
this.peerJid = peerJid;
|
||||
this.resourceJid = Strophe.getResourceFromJid(peerJid);
|
||||
this.videoSpanId = 'participant_' + this.resourceJid;
|
||||
import ConnectionIndicator from './ConnectionIndicator';
|
||||
|
||||
import SmallVideo from "./SmallVideo";
|
||||
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.addRemoteVideoContainer();
|
||||
this.connectionIndicator = new ConnectionIndicator(
|
||||
this, this.peerJid);
|
||||
this.connectionIndicator = new ConnectionIndicator(this, id);
|
||||
this.setDisplayName();
|
||||
var nickfield = document.createElement('span');
|
||||
nickfield.className = "nick";
|
||||
nickfield.appendChild(document.createTextNode(this.resourceJid));
|
||||
nickfield.appendChild(document.createTextNode(id));
|
||||
this.container.appendChild(nickfield);
|
||||
this.bindHoverHandler();
|
||||
this.flipX = false;
|
||||
this.isLocal = false;
|
||||
SmallVideo.call(this);
|
||||
}
|
||||
|
||||
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
|
@ -30,18 +32,20 @@ RemoteVideo.prototype.constructor = RemoteVideo;
|
|||
|
||||
RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
||||
this.container = RemoteVideo.createContainer(this.videoSpanId);
|
||||
if (APP.xmpp.isModerator())
|
||||
if (APP.conference.isModerator) {
|
||||
this.addRemoteVideoMenu();
|
||||
AudioLevels.updateAudioLevelCanvas(this.peerJid, this.VideoLayout);
|
||||
}
|
||||
let {thumbWidth, thumbHeight} = this.VideoLayout.calculateThumbnailSize();
|
||||
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
|
||||
|
||||
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>.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
@ -60,7 +64,7 @@ if (!interfaceConfig.filmStripOnly) {
|
|||
|
||||
var popupmenuElement = document.createElement('ul');
|
||||
popupmenuElement.className = 'popupmenu';
|
||||
popupmenuElement.id = 'remote_popupmenu_' + this.getResourceJid();
|
||||
popupmenuElement.id = `remote_popupmenu_${this.id}`;
|
||||
spanElement.appendChild(popupmenuElement);
|
||||
|
||||
var muteMenuItem = document.createElement('li');
|
||||
|
@ -88,7 +92,7 @@ if (!interfaceConfig.filmStripOnly) {
|
|||
event.preventDefault();
|
||||
}
|
||||
var isMute = !!self.isMuted;
|
||||
APP.xmpp.setMute(self.peerJid, !isMute);
|
||||
self.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, self.id);
|
||||
|
||||
popupmenuElement.setAttribute('style', 'display:none;');
|
||||
|
||||
|
@ -117,7 +121,7 @@ if (!interfaceConfig.filmStripOnly) {
|
|||
"data-i18n='videothumbnail.kick'> </div>";
|
||||
ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
|
||||
ejectLinkItem.onclick = function(){
|
||||
APP.xmpp.eject(self.peerJid);
|
||||
self.emitter.emit(UIEvents.USER_KICKED, self.id);
|
||||
popupmenuElement.setAttribute('style', 'display:none;');
|
||||
};
|
||||
|
||||
|
@ -157,48 +161,49 @@ RemoteVideo.prototype.removeRemoteStreamElement =
|
|||
select.remove();
|
||||
|
||||
console.info((isVideo ? "Video" : "Audio") +
|
||||
" removed " + this.getResourceJid(), select);
|
||||
" removed " + this.id, select);
|
||||
|
||||
if (isVideo)
|
||||
this.VideoLayout.updateRemovedVideo(this.getResourceJid());
|
||||
this.VideoLayout.updateRemovedVideo(this.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes RemoteVideo from the page.
|
||||
*/
|
||||
RemoteVideo.prototype.remove = function () {
|
||||
console.log("Remove thumbnail", this.peerJid);
|
||||
console.log("Remove thumbnail", this.id);
|
||||
this.removeConnectionIndicator();
|
||||
// Make sure that the large video is updated if are removing its
|
||||
// corresponding small video.
|
||||
this.VideoLayout.updateRemovedVideo(this.getResourceJid());
|
||||
this.VideoLayout.updateRemovedVideo(this.id);
|
||||
// Remove whole container
|
||||
if (this.container.parentNode)
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
|
||||
|
||||
var webRtcStream = stream.getOriginalStream();
|
||||
var isVideo = stream.isVideoStream();
|
||||
var isVideo = stream.isVideoTrack();
|
||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var resourceJid = this.getResourceJid();
|
||||
|
||||
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
|
||||
// when video playback starts
|
||||
var onPlayingHandler = function () {
|
||||
// FIXME: why do i have to do this for FF?
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
APP.RTC.attachMediaStream(sel, webRtcStream);
|
||||
//FIXME: weshould use the lib here
|
||||
//APP.RTC.attachMediaStream(sel, webRtcStream);
|
||||
}
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
sel = self.selectVideoElement();
|
||||
}
|
||||
self.VideoLayout.videoactive(sel, resourceJid);
|
||||
self.VideoLayout.videoactive(sel, self.id);
|
||||
sel[0].onplaying = null;
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
// '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) {
|
||||
if (!this.container)
|
||||
if (!this.container) {
|
||||
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(
|
||||
webRtcStream, function () {
|
||||
console.log('stream ended', this);
|
||||
this.stream = stream;
|
||||
|
||||
self.removeRemoteStreamElement(webRtcStream, isVideo, newElementId);
|
||||
});
|
||||
let isVideo = stream.isVideoTrack();
|
||||
|
||||
// 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>
|
||||
// and it does not give event instance as an argument,
|
||||
|
@ -254,14 +238,39 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
|||
return false;
|
||||
};
|
||||
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
|
||||
if (RTCBrowserType.isTemasysPluginUsed())
|
||||
sel = $('#' + newElementId);
|
||||
sel[0].onclick = onClickHandler;
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
sel = $(`#${newElementId}`);
|
||||
}
|
||||
sel.click(onClickHandler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show/hide peer container for the given resourceJid.
|
||||
* Show/hide peer container for the given id.
|
||||
*/
|
||||
RemoteVideo.prototype.showPeerContainer = function (state) {
|
||||
if (!this.container)
|
||||
|
@ -275,10 +284,10 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
|
|||
resizeThumbnails = true;
|
||||
$(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
|
||||
// it's in the lastN set
|
||||
this.showAvatar(undefined);
|
||||
this.updateView();
|
||||
}
|
||||
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
|
||||
// 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 () {
|
||||
if (this.connectionIndicator)
|
||||
this.connectionIndicator.remove();
|
||||
|
@ -311,12 +326,11 @@ RemoteVideo.prototype.hideConnectionIndicator = function () {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
|
||||
var muteMenuItem
|
||||
= $('#remote_popupmenu_' + this.getResourceJid() + '>li>a.mutelink');
|
||||
var muteMenuItem = $(`#remote_popupmenu_${this.id}>li>a.mutelink`);
|
||||
|
||||
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
|
||||
|
||||
|
@ -399,13 +413,11 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
|
|||
nameSpan.className = 'displayname';
|
||||
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
|
||||
|
||||
|
||||
if (displayName && displayName.length > 0) {
|
||||
nameSpan.innerText = displayName;
|
||||
nameSpan.innerHTML = displayName;
|
||||
}
|
||||
else
|
||||
nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
||||
|
||||
nameSpan.innerHTML = interfaceConfig.DEFAULT_REMOTE_DISPLAY_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) {
|
||||
var container = document.createElement('span');
|
||||
container.id = spanId;
|
||||
|
@ -439,4 +444,4 @@ RemoteVideo.createContainer = function (spanId) {
|
|||
};
|
||||
|
||||
|
||||
module.exports = RemoteVideo;
|
||||
export default RemoteVideo;
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
/* global $, APP, require */
|
||||
/* jshint -W101 */
|
||||
var Avatar = require("../avatar/Avatar");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
import Avatar from "../avatar/Avatar";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
||||
|
||||
function SmallVideo() {
|
||||
this.isMuted = false;
|
||||
this.hasAvatar = false;
|
||||
this.isVideoMuted = false;
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
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) {
|
||||
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
|
||||
if (isShow) {
|
||||
|
@ -57,7 +68,7 @@ SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
this.videoType = videoType;
|
||||
|
@ -106,9 +117,10 @@ SmallVideo.prototype.setPresenceStatus = function (statusMsg) {
|
|||
* Creates an audio or video element for a particular MediaStream.
|
||||
*/
|
||||
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');
|
||||
if (isVideo) {
|
||||
element.setAttribute("muted", "true");
|
||||
|
@ -118,8 +130,7 @@ SmallVideo.createStreamElement = function (stream) {
|
|||
element.autoplay = true;
|
||||
}
|
||||
|
||||
element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') +
|
||||
APP.RTC.getStreamID(stream.getOriginalStream());
|
||||
element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
|
||||
|
||||
element.onplay = function () {
|
||||
console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t",
|
||||
|
@ -144,8 +155,8 @@ SmallVideo.prototype.bindHoverHandler = function () {
|
|||
function () {
|
||||
// If the video has been "pinned" by the user we want to
|
||||
// keep the display name on place.
|
||||
if (!LargeVideo.isLargeVideoVisible() ||
|
||||
!LargeVideo.isCurrentlyOnLarge(self.getResourceJid()))
|
||||
if (!self.VideoLayout.isLargeVideoVisible() ||
|
||||
!self.VideoLayout.isCurrentlyOnLarge(self.id))
|
||||
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) {
|
||||
this.showAvatar(isMuted);
|
||||
SmallVideo.prototype.setMutedView = function(isMuted) {
|
||||
this.isVideoMuted = isMuted;
|
||||
this.updateView();
|
||||
|
||||
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
|
||||
|
||||
|
@ -232,43 +245,9 @@ SmallVideo.prototype.showVideoIndicator = function(isMuted) {
|
|||
}
|
||||
|
||||
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 () {
|
||||
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
|
||||
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
|
||||
|
@ -316,13 +295,15 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
|
|||
};
|
||||
|
||||
SmallVideo.prototype.selectVideoElement = function () {
|
||||
var videoElem = APP.RTC.getVideoElementName();
|
||||
var videoElemName;
|
||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
||||
return $('#' + this.videoSpanId).find(videoElem);
|
||||
videoElemName = 'video';
|
||||
return $('#' + this.videoSpanId).find(videoElemName);
|
||||
} else {
|
||||
videoElemName = 'object';
|
||||
var matching = $('#' + this.videoSpanId +
|
||||
(this.isLocal ? '>>' : '>') +
|
||||
videoElem + '>param[value="video"]');
|
||||
videoElemName + '>param[value="video"]');
|
||||
if (matching.length < 2) {
|
||||
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) {
|
||||
if(!isFocused) {
|
||||
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
|
||||
* 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.peerJid) {
|
||||
if (this.id) {
|
||||
// Init avatar
|
||||
this.avatarChanged(Avatar.getAvatarUrl(this.peerJid));
|
||||
this.avatarChanged(Avatar.getAvatarUrl(this.id));
|
||||
} else {
|
||||
console.error("Unable to init avatar - no peerjid", this);
|
||||
console.error("Unable to init avatar - no id", this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var resourceJid = this.getResourceJid();
|
||||
var video = this.selectVideoElement();
|
||||
let video = this.selectVideoElement();
|
||||
|
||||
var avatar = $('#avatar_' + resourceJid);
|
||||
let avatar = $(`#avatar_${this.id}`);
|
||||
|
||||
if (show === undefined || show === null) {
|
||||
if (!this.isLocal &&
|
||||
!this.VideoLayout.isInLastN(resourceJid)) {
|
||||
show = true;
|
||||
} else {
|
||||
// We want to show the avatar when the video is muted or not exists
|
||||
// that is when 'true' or 'null' is returned
|
||||
show = APP.RTC.isVideoMuted(this.peerJid) !== false;
|
||||
}
|
||||
var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
|
||||
|
||||
var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
|
||||
var showAvatar;
|
||||
if ((!this.isLocal
|
||||
&& !this.VideoLayout.isInLastN(this.id))
|
||||
|| this.isVideoMuted) {
|
||||
showAvatar = true;
|
||||
} else {
|
||||
// We want to show the avatar when the video is muted or not exists
|
||||
// that is when 'true' or 'null' is returned
|
||||
showAvatar = !this.stream || this.stream.isMuted();
|
||||
}
|
||||
|
||||
if (LargeVideo.showAvatar(resourceJid, show)) {
|
||||
setVisibility(avatar, false);
|
||||
setVisibility(video, false);
|
||||
} else {
|
||||
if (video && video.length > 0) {
|
||||
setVisibility(video, !show);
|
||||
}
|
||||
setVisibility(avatar, show);
|
||||
showAvatar = showAvatar && !isCurrentlyOnLarge;
|
||||
|
||||
if (video && video.length > 0) {
|
||||
setVisibility(video, showVideo);
|
||||
}
|
||||
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 resourceJid = this.getResourceJid();
|
||||
var avatar = $('#avatar_' + resourceJid);
|
||||
var avatar = $('#avatar_' + this.id);
|
||||
this.hasAvatar = true;
|
||||
|
||||
// set the avatar in the thumbnail
|
||||
if (avatar && avatar.length > 0) {
|
||||
avatar[0].src = thumbUrl;
|
||||
avatar[0].src = avatarUrl;
|
||||
} else {
|
||||
if (thumbnail && thumbnail.length > 0) {
|
||||
avatar = document.createElement('img');
|
||||
avatar.id = 'avatar_' + resourceJid;
|
||||
avatar.id = 'avatar_' + this.id;
|
||||
avatar.className = 'userAvatar';
|
||||
avatar.src = thumbUrl;
|
||||
avatar.src = avatarUrl;
|
||||
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 = {
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@ module.exports = {
|
|||
// This implements the actual choice of an entry in the list based on
|
||||
// roomName. Please consider the implications for existing deployments
|
||||
// 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 idx = n % config.boshList.length;
|
||||
var attemptFirstAddress;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
var EventEmitter = require("events");
|
||||
var eventEmitter = new EventEmitter();
|
||||
var CQEvents = require("../../service/connectionquality/CQEvents");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var StatisticsEvents = require("../../service/statistics/Events");
|
||||
|
||||
/**
|
||||
|
@ -18,69 +17,47 @@ var stats = {};
|
|||
*/
|
||||
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
|
||||
* @param stats
|
||||
* @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
|
||||
*/
|
||||
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 {
|
||||
bitrate: {
|
||||
download: stats.bitrate_download,
|
||||
upload: stats.bitrate_upload
|
||||
download: extractedStats.bitrate_download,
|
||||
upload: extractedStats.bitrate_upload
|
||||
},
|
||||
packetLoss: {
|
||||
total: stats.packetLoss_total,
|
||||
download: stats.packetLoss_download,
|
||||
upload: stats.packetLoss_upload
|
||||
total: extractedStats.packetLoss_total,
|
||||
download: extractedStats.packetLoss_download,
|
||||
upload: extractedStats.packetLoss_upload
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var ConnectionQuality = {
|
||||
init: function () {
|
||||
APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats);
|
||||
APP.statistics.addListener(StatisticsEvents.CONNECTION_STATS,
|
||||
this.updateLocalStats);
|
||||
APP.statistics.addListener(StatisticsEvents.STOP,
|
||||
this.stopSendingStats);
|
||||
APP.statistics.addListener(
|
||||
StatisticsEvents.CONNECTION_STATS, this.updateLocalStats
|
||||
);
|
||||
APP.statistics.addListener(
|
||||
StatisticsEvents.STOP, this.stopSendingStats
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -90,33 +67,30 @@ var ConnectionQuality = {
|
|||
updateLocalStats: function (data) {
|
||||
stats = data;
|
||||
eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
|
||||
if (!sendIntervalId) {
|
||||
startSendingStats();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates remote statistics
|
||||
* @param jid the jid associated with the statistics
|
||||
* @param id the id associated with the statistics
|
||||
* @param data the statistics
|
||||
*/
|
||||
updateRemoteStats: function (jid, data) {
|
||||
if (!data || !data.packetLoss_total) {
|
||||
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null);
|
||||
updateRemoteStats: function (id, data) {
|
||||
data = parseMUCStats(data);
|
||||
if (!data || !data.packetLoss || !data.packetLoss.total) {
|
||||
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, id, null, null);
|
||||
return;
|
||||
}
|
||||
remoteStats[jid] = parseMUCStats(data);
|
||||
remoteStats[id] = data;
|
||||
|
||||
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED,
|
||||
jid, 100 - data.packetLoss_total, remoteStats[jid]);
|
||||
eventEmitter.emit(
|
||||
CQEvents.REMOTESTATS_UPDATED, id, 100 - data.packetLoss_total, remoteStats[id]
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops statistics sending.
|
||||
*/
|
||||
stopSendingStats: function () {
|
||||
clearInterval(sendIntervalId);
|
||||
sendIntervalId = null;
|
||||
//notify UI about stopping statistics gathering
|
||||
eventEmitter.emit(CQEvents.STOP);
|
||||
},
|
||||
|
@ -127,11 +101,29 @@ var ConnectionQuality = {
|
|||
getStats: function () {
|
||||
return stats;
|
||||
},
|
||||
|
||||
|
||||
addListener: function (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 DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var ScreenObtainer = require("./ScreenObtainer");
|
||||
import DSEvents from '../../service/desktopsharing/DesktopSharingEventTypes';
|
||||
|
||||
const TrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* Indicates that desktop stream is currently in use (for toggle purpose).
|
||||
|
@ -20,22 +18,19 @@ var isUsingScreenStream = 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();
|
||||
|
||||
function streamSwitchDone() {
|
||||
switchInProgress = false;
|
||||
eventEmitter.emit(
|
||||
DesktopSharingEventTypes.SWITCHING_DONE,
|
||||
isUsingScreenStream);
|
||||
eventEmitter.emit(DSEvents.SWITCHING_DONE, isUsingScreenStream);
|
||||
}
|
||||
|
||||
function newStreamCreated(stream) {
|
||||
eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
||||
stream, isUsingScreenStream, streamSwitchDone);
|
||||
function newStreamCreated(track) {
|
||||
eventEmitter.emit(DSEvents.NEW_STREAM_CREATED, track, streamSwitchDone);
|
||||
}
|
||||
|
||||
function getVideoStreamFailed(error) {
|
||||
|
@ -50,36 +45,31 @@ function getDesktopStreamFailed(error) {
|
|||
switchInProgress = false;
|
||||
}
|
||||
|
||||
function onEndedHandler(stream) {
|
||||
function onEndedHandler() {
|
||||
if (!switchInProgress && isUsingScreenStream) {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
}
|
||||
|
||||
APP.RTC.removeMediaStreamInactiveHandler(stream, onEndedHandler);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isUsingScreenStream: function () {
|
||||
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
|
||||
* and enabled.
|
||||
*/
|
||||
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) {
|
||||
eventEmitter.on(type, listener);
|
||||
},
|
||||
|
@ -95,43 +85,50 @@ module.exports = {
|
|||
if (switchInProgress) {
|
||||
console.warn("Switch in progress.");
|
||||
return;
|
||||
} else if (!screenObtainer.isSupported()) {
|
||||
} else if (!this.isDesktopSharingEnabled()) {
|
||||
console.warn("Cannot toggle screen sharing: not supported.");
|
||||
return;
|
||||
}
|
||||
switchInProgress = true;
|
||||
|
||||
let type;
|
||||
if (!isUsingScreenStream) {
|
||||
// Switch to desktop stream
|
||||
screenObtainer.obtainStream(
|
||||
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);
|
||||
type = "desktop";
|
||||
} else {
|
||||
// Disable screen stream
|
||||
APP.RTC.getUserMediaWithConstraints(
|
||||
['video'],
|
||||
function (stream) {
|
||||
// We are now using camera stream
|
||||
isUsingScreenStream = false;
|
||||
newStreamCreated(stream);
|
||||
},
|
||||
getVideoStreamFailed,
|
||||
config.resolution || '360'
|
||||
);
|
||||
type = "video";
|
||||
}
|
||||
},
|
||||
/*
|
||||
* Exports the event emitter to allow use by ScreenObtainer. Not for outside
|
||||
* use.
|
||||
*/
|
||||
eventEmitter: eventEmitter
|
||||
};
|
||||
var fail = (error) => {
|
||||
if (type === 'desktop') {
|
||||
getDesktopStreamFailed(error);
|
||||
} else {
|
||||
getVideoStreamFailed(error);
|
||||
}
|
||||
};
|
||||
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: {
|
||||
character: "M",
|
||||
id: "mutePopover",
|
||||
function: APP.UI.toggleAudio
|
||||
function: APP.conference.toggleAudioMuted
|
||||
},
|
||||
84: {
|
||||
character: "T",
|
||||
function: function() {
|
||||
if(!APP.RTC.localAudio.isMuted()) {
|
||||
APP.UI.toggleAudio();
|
||||
}
|
||||
APP.conference.muteAudio(true);
|
||||
}
|
||||
},
|
||||
86: {
|
||||
character: "V",
|
||||
id: "toggleVideoPopover",
|
||||
function: APP.UI.toggleVideo
|
||||
function: APP.conference.toggleVideoMuted
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -67,9 +65,7 @@ var KeyboardShortcut = {
|
|||
$(":focus").is("input[type=password]") ||
|
||||
$(":focus").is("textarea"))) {
|
||||
if(e.which === "T".charCodeAt(0)) {
|
||||
if(APP.RTC.localAudio.isMuted()) {
|
||||
APP.UI.toggleAudio();
|
||||
}
|
||||
APP.conference.muteAudio(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 displayName = '';
|
||||
var userId;
|
||||
var language = null;
|
||||
var callStatsUserName;
|
||||
|
||||
|
||||
function supportsLocalStorage() {
|
||||
try {
|
||||
|
@ -30,25 +28,16 @@ if (supportsLocalStorage()) {
|
|||
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 || '';
|
||||
callStatsUserName = window.localStorage.callStatsUserName;
|
||||
email = window.localStorage.email || '';
|
||||
displayName = window.localStorage.displayname || '';
|
||||
language = window.localStorage.language;
|
||||
} else {
|
||||
console.log("local storage is not supported");
|
||||
userId = generateUniqueId();
|
||||
callStatsUserName = UsernameGenerator.generateUsername();
|
||||
}
|
||||
|
||||
var Settings = {
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
setDisplayName: function (newDisplayName) {
|
||||
if (displayName === newDisplayName) {
|
||||
return displayName;
|
||||
}
|
||||
displayName = newDisplayName;
|
||||
window.localStorage.displayname = displayName;
|
||||
return displayName;
|
||||
|
@ -70,20 +62,16 @@ var Settings = {
|
|||
return displayName;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns fake username for callstats
|
||||
* @returns {string} fake username for callstats
|
||||
*/
|
||||
getCallStatsUserName: function () {
|
||||
return callStatsUserName;
|
||||
},
|
||||
|
||||
setEmail: function (newEmail) {
|
||||
email = newEmail;
|
||||
window.localStorage.email = newEmail;
|
||||
return email;
|
||||
},
|
||||
|
||||
getEmail: function () {
|
||||
return email;
|
||||
},
|
||||
|
||||
getSettings: function () {
|
||||
return {
|
||||
email: email,
|
||||
|
@ -92,10 +80,11 @@ var Settings = {
|
|||
language: language
|
||||
};
|
||||
},
|
||||
getLanguage () {
|
||||
return language;
|
||||
},
|
||||
setLanguage: function (lang) {
|
||||
language = lang;
|
||||
window.localStorage.language = lang;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Settings;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
/* global config */
|
||||
|
||||
var ScriptUtil = require('../util/ScriptUtil');
|
||||
/* global config JitsiMeetJS */
|
||||
|
||||
// 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
|
||||
|
@ -11,37 +9,38 @@ var ScriptUtil = require('../util/ScriptUtil');
|
|||
// its implementation asynchronously anyway so it makes sense to append the
|
||||
// loading on our side rather than prepend it.
|
||||
if (config.disableThirdPartyRequests !== true) {
|
||||
ScriptUtil.loadScript(
|
||||
JitsiMeetJS.util.ScriptUtil.loadScript(
|
||||
'analytics.js?v=1',
|
||||
/* async */ true,
|
||||
/* prepend */ false);
|
||||
}
|
||||
|
||||
// NoopAnalytics
|
||||
function NoopAnalytics() {}
|
||||
|
||||
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.
|
||||
class NoopAnalytics {
|
||||
sendEvent () {}
|
||||
}
|
||||
|
||||
AnalyticsAdapter.prototype.sendEvent = function (action, data) {
|
||||
var a = this.analytics;
|
||||
// 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.
|
||||
|
||||
if (a === null || typeof a === 'undefined') {
|
||||
var AnalyticsImpl = window.Analytics || NoopAnalytics;
|
||||
class AnalyticsAdapter {
|
||||
constructor () {
|
||||
}
|
||||
|
||||
this.analytics = a = new AnalyticsImpl();
|
||||
}
|
||||
try {
|
||||
a.sendEvent.apply(a, arguments);
|
||||
} catch (ignored) {}
|
||||
};
|
||||
sendEvent (...args) {
|
||||
var a = this.analytics;
|
||||
|
||||
module.exports = new AnalyticsAdapter();
|
||||
if (a === null || typeof a === 'undefined') {
|
||||
var AnalyticsImpl = window.Analytics || NoopAnalytics;
|
||||
|
||||
this.analytics = a = new AnalyticsImpl();
|
||||
}
|
||||
try {
|
||||
a.sendEvent(...args);
|
||||
} catch (ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
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.baselineAudioLevelsReport = null;
|
||||
this.currentAudioLevelsReport = null;
|
||||
this.currentStatsReport = null;
|
||||
this.baselineStatsReport = null;
|
||||
this.audioLevelsIntervalId = null;
|
||||
|
@ -252,10 +251,7 @@ StatsCollector.prototype.start = function ()
|
|||
results = report.result();
|
||||
}
|
||||
//console.error("Got interval report", results);
|
||||
self.currentAudioLevelsReport = results;
|
||||
self.processAudioLevelReport();
|
||||
self.baselineAudioLevelsReport =
|
||||
self.currentAudioLevelsReport;
|
||||
self.baselineAudioLevelsReport = results;
|
||||
},
|
||||
self.errorCallback
|
||||
);
|
||||
|
@ -385,7 +381,7 @@ StatsCollector.prototype.addStatsToBeLogged = function (reports) {
|
|||
|
||||
StatsCollector.prototype.logStats = function () {
|
||||
|
||||
if(!APP.xmpp.sendLogs(this.statsToBeLogged))
|
||||
if(!APP.conference._room.xmpp.sendLogs(this.statsToBeLogged))
|
||||
return;
|
||||
// Reset the stats
|
||||
this.statsToBeLogged.stats = {};
|
||||
|
@ -501,7 +497,7 @@ StatsCollector.prototype.processStatsReport = function () {
|
|||
var ssrc = getStatValue(now, 'ssrc');
|
||||
if(!ssrc)
|
||||
continue;
|
||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
||||
var jid = APP.conference._room.room.getJidBySSRC(ssrc);
|
||||
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
continue;
|
||||
|
@ -647,76 +643,22 @@ StatsCollector.prototype.processStatsReport = function () {
|
|||
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,
|
||||
{
|
||||
"bitrate": PeerStats.bitrate,
|
||||
"packetLoss": PeerStats.packetLoss,
|
||||
"bandwidth": PeerStats.bandwidth,
|
||||
"resolution": resolutions,
|
||||
"resolution": idResolution,
|
||||
"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.
|
||||
*/
|
||||
var LocalStats = require("./LocalStatsCollector.js");
|
||||
var RTPStats = require("./RTPStatsCollector.js");
|
||||
var EventEmitter = require("events");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var CallStats = require("./CallStats");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var StatisticsEvents = require("../../service/statistics/Events");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
|
||||
var localStats = null;
|
||||
|
||||
var rtpStats = null;
|
||||
|
||||
function stopLocal() {
|
||||
if (localStats) {
|
||||
localStats.stop();
|
||||
localStats = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRemote() {
|
||||
if (rtpStats) {
|
||||
rtpStats.stop();
|
||||
|
@ -41,26 +30,14 @@ function startRemoteStats (peerconnection) {
|
|||
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) {
|
||||
CallStats.sendTerminateEvent();
|
||||
stopRemote();
|
||||
if(onUnload) {
|
||||
stopLocal();
|
||||
if (onUnload) {
|
||||
eventEmitter.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
var statistics = {
|
||||
export default {
|
||||
/**
|
||||
* Indicates that this audio level is for local jid.
|
||||
* @type {string}
|
||||
|
@ -74,89 +51,21 @@ var statistics = {
|
|||
eventEmitter.removeListener(type, listener);
|
||||
},
|
||||
stop: function () {
|
||||
stopLocal();
|
||||
stopRemote();
|
||||
if(eventEmitter)
|
||||
{
|
||||
if (eventEmitter) {
|
||||
eventEmitter.removeAllListeners();
|
||||
}
|
||||
},
|
||||
stopRemoteStatistics: function()
|
||||
{
|
||||
stopRemote();
|
||||
},
|
||||
start: function () {
|
||||
APP.RTC.addStreamListener(onStreamCreated,
|
||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
||||
APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE,
|
||||
onDisposeConference);
|
||||
const xmpp = APP.conference._room.xmpp;
|
||||
xmpp.addListener(
|
||||
XMPPEvents.DISPOSE_CONFERENCE,
|
||||
onDisposeConference
|
||||
);
|
||||
//FIXME: we may want to change CALL INCOMING event to
|
||||
// onnegotiationneeded
|
||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
||||
xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
||||
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 */
|
||||
var i18n = require("i18next-client");
|
||||
var languages = require("../../service/translation/languages");
|
||||
var Settings = require("../settings/Settings");
|
||||
var DEFAULT_LANG = languages.EN;
|
||||
|
||||
i18n.addPostProcessor("resolveAppName", function(value, key, options) {
|
||||
|
@ -68,7 +67,7 @@ function initCompleted(t) {
|
|||
$("[data-i18n]").i18n();
|
||||
}
|
||||
|
||||
function checkForParameter() {
|
||||
function getLangFromQuery() {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
|
@ -82,27 +81,11 @@ function checkForParameter() {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
init: function (lang) {
|
||||
var options = defaultOptions;
|
||||
init: function (settingsLang) {
|
||||
let options = defaultOptions;
|
||||
|
||||
|
||||
if(!lang)
|
||||
{
|
||||
lang = checkForParameter();
|
||||
if(!lang)
|
||||
{
|
||||
var settings = Settings.getSettings();
|
||||
if(settings)
|
||||
lang = settings.language;
|
||||
|
||||
if(!lang && config.defaultLanguage)
|
||||
{
|
||||
lang = config.defaultLanguage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(lang) {
|
||||
let lang = getLangFromQuery() || settingsLang || config.defaultLanguage;
|
||||
if (lang) {
|
||||
options.lng = lang;
|
||||
}
|
||||
|
||||
|
@ -124,8 +107,7 @@ module.exports = {
|
|||
},
|
||||
generateTranslationHTML: function (key, options) {
|
||||
var str = "<span data-i18n=\"" + key + "\"";
|
||||
if(options)
|
||||
{
|
||||
if (options) {
|
||||
str += " data-i18n-options=\"" + JSON.stringify(options) + "\"";
|
||||
}
|
||||
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
|
||||
|
@ -417,13 +417,9 @@ var names = [
|
|||
* Generate random username.
|
||||
* @returns {string} random username
|
||||
*/
|
||||
function generateUsername () {
|
||||
export function generateUsername () {
|
||||
var name = RandomUtil.randomElement(names);
|
||||
var suffix = RandomUtil.randomAlphanumStr(3);
|
||||
|
||||
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",
|
||||
"precommit-hook": "3.0.0",
|
||||
"uglify-js": "2.4.24",
|
||||
"clean-css": "*"
|
||||
"clean-css": "*",
|
||||
"babelify": "*",
|
||||
"babel-preset-es2015": "*",
|
||||
"babel-polyfill": "*"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
@ -54,9 +57,15 @@
|
|||
],
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"browserify-shim"
|
||||
"browserify-shim",
|
||||
["babelify", {
|
||||
"ignore": "node_modules"
|
||||
}]
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": ["es2015"]
|
||||
},
|
||||
"browser": {
|
||||
"jquery": "./node_modules/jquery/dist/jquery.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",
|
||||
SELECTED_ENDPOINT: "UI.selected_endpoint",
|
||||
PINNED_ENDPOINT: "UI.pinned_endpoint",
|
||||
LARGEVIDEO_INIT: "UI.largevideo_init",
|
||||
/**
|
||||
* Notifies interested parties when the film strip (remote video's panel)
|
||||
* is hidden (toggled) or shown (un-toggled).
|
||||
* Notifies that local user created text message.
|
||||
*/
|
||||
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 = {
|
||||
INIT: "ds.init",
|
||||
|
||||
export default {
|
||||
SWITCHING_DONE: "ds.switching_done",
|
||||
|
||||
NEW_STREAM_CREATED: "ds.new_stream_created",
|
||||
|
@ -11,5 +9,3 @@ var DesktopSharingEventTypes = {
|
|||
*/
|
||||
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',
|
||||
PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
|
||||
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
|
||||
READY_TO_JOIN: 'xmpp.ready_to_join'
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue