Merge branch 'jitsi-meet-new'

This commit is contained in:
damencho 2016-01-28 14:10:50 -06:00
commit c0dde18e6b
92 changed files with 37412 additions and 14315 deletions

10
.editorconfig Normal file
View File

@ -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
.gitattributes vendored
View File

@ -1 +1,2 @@
*.bundle.js -text -diff *.bundle.js -text -diff
lib-jitsi-meet.js -text -diff

View File

@ -2,7 +2,10 @@ node_modules
libs libs
debian debian
analytics.js analytics.js
lib-jitsi-meet.js
modules/xmpp/strophe.emuc.js modules/xmpp/strophe.emuc.js
modules/UI/prezi/Prezi.js modules/UI/prezi/Prezi.js
modules/RTC/adapter.screenshare.js modules/RTC/adapter.screenshare.js
modules/statistics/*
modules/UI/videolayout/*

View File

@ -15,5 +15,6 @@
"newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()` "newcap": true, // true: Require capitalization of all constructor functions e.g. `new F()`
"maxlen": 80, // {int} Max number of characters per line "maxlen": 80, // {int} Max number of characters per line
"latedef": false, //This option prohibits the use of a variable before it was defined "latedef": false, //This option prohibits the use of a variable before it was defined
"laxbreak": true //Ignore line breaks around "=", "==", "&&", etc. "laxbreak": true, //Ignore line breaks around "=", "==", "&&", etc.
"esnext": true //support ES2015
} }

View File

@ -8,10 +8,13 @@ DEPLOY_DIR = libs
BROWSERIFY_FLAGS = -d BROWSERIFY_FLAGS = -d
OUTPUT_DIR = . OUTPUT_DIR = .
all: compile uglify deploy clean all: update-deps compile uglify deploy clean
update-deps:
$(NPM) update
compile: compile:
$(NPM) update && $(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js $(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js
clean: clean:
rm -f $(OUTPUT_DIR)/app.bundle.* rm -f $(OUTPUT_DIR)/app.bundle.*

122
app.js
View File

@ -1,48 +1,99 @@
/* jshint -W117 */ /* global $, JitsiMeetJS, config */
/* application specific logic */ /* application specific logic */
require("jquery"); import "babel-polyfill";
require("jquery-ui"); import "jquery";
require("strophe"); import "jquery-ui";
require("strophe-disco"); import "strophe";
require("strophe-caps"); import "strophe-disco";
require("tooltip"); import "strophe-caps";
require("popover"); import "tooltip";
import "popover";
import "jQuery-Impromptu";
import "autosize";
window.toastr = require("toastr"); window.toastr = require("toastr");
require("jQuery-Impromptu");
require("autosize");
var APP = import URLProcessor from "./modules/config/URLProcessor";
{ import RoomnameGenerator from './modules/util/RoomnameGenerator';
init: function () {
this.UI = require("./modules/UI/UI"); import UI from "./modules/UI/UI";
this.API = require("./modules/API/API"); import statistics from "./modules/statistics/statistics";
import settings from "./modules/settings/Settings";
import conference from './conference';
import API from './modules/API/API';
import UIEvents from './service/UI/UIEvents';
function buildRoomName () {
let path = window.location.pathname;
let roomName;
// determinde the room node from the url
// TODO: just the roomnode or the whole bare jid?
if (config.getroomnode && typeof config.getroomnode === 'function') {
// custom function might be responsible for doing the pushstate
roomName = config.getroomnode(path);
} else {
/* fall back to default strategy
* this is making assumptions about how the URL->room mapping happens.
* It currently assumes deployment at root, with a rewrite like the
* following one (for nginx):
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
*/
if (path.length > 1) {
roomName = path.substr(1).toLowerCase();
} else {
let word = RoomnameGenerator.generateRoomWithoutSeparator();
roomName = word.toLowerCase();
window.history.pushState(
'VideoChat', `Room: ${word}`, window.location.pathname + word
);
}
}
return roomName;
}
const APP = {
UI,
statistics,
settings,
conference,
API,
init () {
this.connectionquality = this.connectionquality =
require("./modules/connectionquality/connectionquality"); require("./modules/connectionquality/connectionquality");
this.statistics = require("./modules/statistics/statistics");
this.RTC = require("./modules/RTC/RTC");
this.desktopsharing = this.desktopsharing =
require("./modules/desktopsharing/desktopsharing"); require("./modules/desktopsharing/desktopsharing");
this.xmpp = require("./modules/xmpp/xmpp");
this.keyboardshortcut = this.keyboardshortcut =
require("./modules/keyboardshortcut/keyboardshortcut"); require("./modules/keyboardshortcut/keyboardshortcut");
this.translation = require("./modules/translation/translation"); this.translation = require("./modules/translation/translation");
this.settings = require("./modules/settings/Settings");
//this.DTMF = require("./modules/DTMF/DTMF");
this.members = require("./modules/members/MemberList");
this.configFetch = require("./modules/config/HttpConfigFetch"); this.configFetch = require("./modules/config/HttpConfigFetch");
} }
}; };
function init() { function init() {
var isUIReady = APP.UI.start();
if (isUIReady) {
APP.conference.init({roomName: buildRoomName()}).then(function () {
APP.UI.initConference();
APP.desktopsharing.init(); APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {
APP.RTC.start(); APP.translation.setLanguage(language);
APP.xmpp.start(); APP.settings.setLanguage(language);
APP.statistics.start(); });
APP.connectionquality.init();
APP.keyboardshortcut.init(); APP.desktopsharing.init(JitsiMeetJS.isDesktopSharingEnabled());
APP.members.start(); 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. * will be displayed to the user.
*/ */
function obtainConfigAndInit() { function obtainConfigAndInit() {
var roomName = APP.UI.getRoomNode(); let roomName = APP.conference.roomName;
if (config.configLocation) { if (config.configLocation) {
APP.configFetch.obtainConfig( APP.configFetch.obtainConfig(
@ -84,23 +135,18 @@ function obtainConfigAndInit() {
$(document).ready(function () { $(document).ready(function () {
console.log("(TIME) document ready:\t", window.performance.now()); console.log("(TIME) document ready:\t", window.performance.now());
var URLProcessor = require("./modules/config/URLProcessor");
URLProcessor.setConfigParametersFromUrl(); URLProcessor.setConfigParametersFromUrl();
APP.init(); APP.init();
APP.translation.init(); APP.translation.init(settings.getLanguage());
if(APP.API.isEnabled()) APP.API.init();
APP.API.init();
APP.UI.start(obtainConfigAndInit);
obtainConfigAndInit();
}); });
$(window).bind('beforeunload', function () { $(window).bind('beforeunload', function () {
if(APP.API.isEnabled()) APP.API.dispose();
APP.API.dispose();
}); });
module.exports = APP; module.exports = APP;

820
conference.js Normal file
View File

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

107
connection.js Normal file
View File

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

View File

@ -34,7 +34,7 @@
} }
#remoteVideos .videocontainer { #remoteVideos .videocontainer {
display: inline-block; display: none;
background-color: black; background-color: black;
background-size: contain; background-size: contain;
border-radius:8px; border-radius:8px;
@ -378,7 +378,7 @@
padding-right:2px; padding-right:2px;
height:38px; height:38px;
width:auto; 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: 1px solid rgba(256, 256, 256, 0.2);
border-radius: 6px; border-radius: 6px;
pointer-events: auto; pointer-events: auto;
@ -407,7 +407,7 @@
pointer-events: none; pointer-events: none;
} }
#activeSpeaker { #dominantSpeaker {
visibility: hidden; visibility: hidden;
width: 150px; width: 150px;
height: 150px; height: 150px;
@ -416,7 +416,7 @@
position: relative; position: relative;
} }
#activeSpeakerAudioLevel { #dominantSpeakerAudioLevel {
position: absolute; position: absolute;
top: 0px; top: 0px;
left: 0px; left: 0px;
@ -428,7 +428,7 @@
display:none !important; display:none !important;
} }
#activeSpeakerAvatar { #dominantSpeakerAvatar {
width: 100px; width: 100px;
height: 100px; height: 100px;
top: 25px; top: 25px;

2
debian/control vendored
View File

@ -35,6 +35,6 @@ Description: Prosody configuration for Jitsi Meet
Package: jitsi-meet-tokens Package: jitsi-meet-tokens
Architecture: all Architecture: all
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly603), libssl-dev, luarocks, jitsi-meet-prosody Depends: ${misc:Depends}, prosody-trunk (>= 1nightly607), libssl-dev, luarocks, jitsi-meet-prosody
Description: Prosody token authentication plugin for Jitsi Meet Description: Prosody token authentication plugin for Jitsi Meet

View File

@ -74,6 +74,11 @@ case "$1" in
if [ -x "/etc/init.d/prosody" ]; then if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d prosody restart invoke-rc.d prosody restart
fi fi
echo "This package requires BOSH Prosody module to be patched !"
echo "Use the following command, after this package has been installed and"
echo "after every prosody-trunk upgrade:"
echo "sudo patch -N /usr/lib/prosody/modules/mod_bosh.lua /usr/share/jitsi-meet/prosody-plugins/mod_bosh.lua.patch"
else else
echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet" echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet"
fi fi

View File

@ -13,6 +13,7 @@
<script>console.log("(TIME) index.html loaded:\t", window.performance.now());</script> <script>console.log("(TIME) index.html loaded:\t", window.performance.now());</script>
<script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path --> <script src="config.js?v=15"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="interface_config.js?v=6"></script> <script src="interface_config.js?v=6"></script>
<script src="libs/lib-jitsi-meet.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script> <script src="libs/app.bundle.min.js?v=139"></script>
<!-- <!--
Link used for inline installation of chrome desktop streaming extension, Link used for inline installation of chrome desktop streaming extension,
@ -139,13 +140,32 @@
</div> </div>
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div> <div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
<div id="videospace"> <div id="videospace">
<div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="etherpad"></div>
<a target="_new"><div class="watermark leftwatermark"></div></a>
<a target="_new"><div class="watermark rightwatermark"></div></a>
<a class="poweredby" href="http://jitsi.org" target="_new">
<span data-i18n="poweredby"></span> jitsi.org
</a>
<div id="dominantSpeaker">
<img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas>
</div>
<div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video>
</div>
<span id="videoConnectionMessage"></span>
</div>
<div id="remoteVideos"> <div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer"> <span id="localVideoContainer" class="videocontainer">
<span id="localNick" class="nick"></span> <span id="localNick" class="nick"></span>
<span id="localVideoWrapper"> <span id="localVideoWrapper">
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated --> <!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
</span> </span>
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio> <audio id="localAudio" autoplay muted></audio>
<span class="focusindicator"></span> <span class="focusindicator"></span>
</span> </span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio> <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>

View File

@ -14,7 +14,7 @@ var interfaceConfig = {
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true, GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
APP_NAME: "Jitsi Meet", APP_NAME: "Jitsi Meet",
INVITATION_POWERED_BY: true, INVITATION_POWERED_BY: true,
ACTIVE_SPEAKER_AVATAR_SIZE: 100, DOMINANT_SPEAKER_AVATAR_SIZE: 100,
TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop', TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop',
'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad', 'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad',
'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip', 'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip',

31780
libs/lib-jitsi-meet.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,6 @@
* applications that embed Jitsi Meet * applications that embed Jitsi Meet
*/ */
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
/** /**
* List of the available commands. * List of the available commands.
* @type {{ * @type {{
@ -23,8 +21,8 @@ var commands = {};
function initCommands() { function initCommands() {
commands = { commands = {
displayName: APP.UI.inputDisplayNameHandler, displayName: APP.UI.inputDisplayNameHandler,
toggleAudio: APP.UI.toggleAudio, toggleAudio: APP.conference.toggleAudioMuted,
toggleVideo: APP.UI.toggleVideo, toggleVideo: APP.conference.toggleVideoMuted,
toggleFilmStrip: APP.UI.toggleFilmStrip, toggleFilmStrip: APP.UI.toggleFilmStrip,
toggleChat: APP.UI.toggleChat, toggleChat: APP.UI.toggleChat,
toggleContactList: APP.UI.toggleContactList toggleContactList: APP.UI.toggleContactList
@ -43,7 +41,7 @@ function initCommands() {
* participantLeft: boolean * participantLeft: boolean
* }} * }}
*/ */
var events = { const events = {
incomingMessage: false, incomingMessage: false,
outgoingMessage:false, outgoingMessage:false,
displayNameChange: false, displayNameChange: false,
@ -51,8 +49,6 @@ var events = {
participantLeft: false participantLeft: false
}; };
var displayName = {};
/** /**
* Processes commands from external application. * Processes commands from external application.
* @param message the object with the command * @param message the object with the command
@ -128,44 +124,42 @@ function processMessage(event) {
} }
} }
function setupListeners() { /**
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, function (from) { * Check whether the API should be enabled or not.
API.triggerEvent("participantJoined", {jid: from}); * @returns {boolean}
}); */
APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, function isEnabled () {
function (from, nick, txt, myjid, stamp) { let hash = location.hash;
if (from != myjid) return hash && hash.indexOf("external") > -1 && window.postMessage;
API.triggerEvent("incomingMessage",
{"from": from, "nick": nick, "message": txt, "stamp": stamp});
});
APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, function (jid) {
API.triggerEvent("participantLeft", {jid: jid});
});
APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
function (jid, newDisplayName) {
var name = displayName[jid];
if(!name || name != newDisplayName) {
API.triggerEvent("displayNameChange",
{jid: jid, displayname: newDisplayName});
displayName[jid] = newDisplayName;
}
});
APP.xmpp.addListener(XMPPEvents.SENDING_CHAT_MESSAGE, function (body) {
APP.API.triggerEvent("outgoingMessage", {"message": body});
});
} }
var API = { /**
/** * Checks whether the event is enabled ot not.
* Check whether the API should be enabled or not. * @param name the name of the event.
* @returns {boolean} * @returns {*}
*/ */
isEnabled: function () { function isEventEnabled (name) {
var hash = location.hash; return events[name];
if (hash && hash.indexOf("external") > -1 && window.postMessage) }
return true;
return false; /**
}, * Sends event object to the external application that has been subscribed
* for that event.
* @param name the name event
* @param object data associated with the event
*/
function triggerEvent (name, object) {
if (isEnabled() && isEventEnabled(name)) {
sendMessage({
type: "event",
action: "result",
event: name,
result: object
});
}
}
export default {
/** /**
* Initializes the APIConnector. Setups message event listeners that will * Initializes the APIConnector. Setups message event listeners that will
* receive information from external applications that embed Jitsi Meet. * receive information from external applications that embed Jitsi Meet.
@ -173,50 +167,85 @@ var API = {
* is initialized. * is initialized.
*/ */
init: function () { init: function () {
if (!isEnabled()) {
return;
}
initCommands(); initCommands();
if (window.addEventListener) { if (window.addEventListener) {
window.addEventListener('message', window.addEventListener('message', processMessage, false);
processMessage, false); } else {
}
else {
window.attachEvent('onmessage', processMessage); window.attachEvent('onmessage', processMessage);
} }
sendMessage({type: "system", loaded: true}); sendMessage({type: "system", loaded: true});
setupListeners();
},
/**
* Checks whether the event is enabled ot not.
* @param name the name of the event.
* @returns {*}
*/
isEventEnabled: function (name) {
return events[name];
}, },
/** /**
* Sends event object to the external application that has been subscribed * Notify external application (if API is enabled) that message was sent.
* for that event. * @param {string} body message body
* @param name the name event
* @param object data associated with the event
*/ */
triggerEvent: function (name, object) { notifySendingChatMessage (body) {
if(this.isEnabled() && this.isEventEnabled(name)) triggerEvent("outgoingMessage", {"message": body});
sendMessage({ },
type: "event", action: "result", event: name, result: object});
/**
* Notify external application (if API is enabled) that
* message was received.
* @param {string} id user id
* @param {string} nick user nickname
* @param {string} body message body
* @param {number} ts message creation timestamp
*/
notifyReceivedChatMessage (id, nick, body, ts) {
if (APP.conference.isLocalId(id)) {
return;
}
triggerEvent(
"incomingMessage",
{"from": id, "nick": nick, "message": body, "stamp": ts}
);
},
/**
* Notify external application (if API is enabled) that
* user joined the conference.
* @param {string} id user id
*/
notifyUserJoined (id) {
triggerEvent("participantJoined", {id});
},
/**
* Notify external application (if API is enabled) that
* user left the conference.
* @param {string} id user id
*/
notifyUserLeft (id) {
triggerEvent("participantLeft", {id});
},
/**
* Notify external application (if API is enabled) that
* user changed their nickname.
* @param {string} id user id
* @param {string} displayName user nickname
*/
notifyDisplayNameChanged (id, displayName) {
triggerEvent("displayNameChange", {id, displayname: displayName});
}, },
/** /**
* Removes the listeners. * Removes the listeners.
*/ */
dispose: function () { dispose: function () {
if(window.removeEventListener) { if (!isEnabled()) {
window.removeEventListener("message", return;
processMessage, false);
} }
else {
if (window.removeEventListener) {
window.removeEventListener("message", processMessage, false);
} else {
window.detachEvent('onmessage', processMessage); window.detachEvent('onmessage', processMessage);
} }
} }
}; };
module.exports = API;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,9 @@
/* global $, config, interfaceConfig */ /* global $, APP, config, interfaceConfig */
/* /*
* Created by Yana Stamcheva on 2/10/15. * Created by Yana Stamcheva on 2/10/15.
*/ */
var messageHandler = require("./util/MessageHandler"); var messageHandler = require("./util/MessageHandler");
var callStats = require("../statistics/CallStats");
var APP = require("../../app");
/** /**
* Constructs the html for the overall feedback window. * Constructs the html for the overall feedback window.
@ -79,7 +77,7 @@ var Feedback = {
init: function () { init: function () {
// CallStats is the way we send feedback, so we don't have to initialise // CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled. // if callstats isn't enabled.
if (!this.isEnabled()) if (!APP.conference.isCallstatsEnabled())
return; return;
$("div.feedbackButton").css("display", "block"); $("div.feedbackButton").css("display", "block");
@ -93,10 +91,7 @@ var Feedback = {
* @return true if the feedback functionality is enabled, false otherwise. * @return true if the feedback functionality is enabled, false otherwise.
*/ */
isEnabled: function() { isEnabled: function() {
// XXX callStats.isEnabled() indicates whether we are allowed to attempt return APP.conference.isCallstatsEnabled();
// to integrate callstats.io. Whether our attempt was/is/will be
// successful is a different question.
return callStats.isEnabled();
}, },
/** /**
* Opens the feedback window. * Opens the feedback window.
@ -122,7 +117,7 @@ var Feedback = {
// If the feedback is less than 3 stars we're going to // If the feedback is less than 3 stars we're going to
// ask the user for more information. // ask the user for more information.
if (Feedback.feedbackScore > 3) { if (Feedback.feedbackScore > 3) {
callStats.sendFeedback(Feedback.feedbackScore, ""); APP.conference.sendFeedback(Feedback.feedbackScore, "");
if (feedbackWindowCallback) if (feedbackWindowCallback)
feedbackWindowCallback(); feedbackWindowCallback();
else else
@ -164,7 +159,7 @@ var Feedback = {
= document.getElementById("feedbackTextArea").value; = document.getElementById("feedbackTextArea").value;
if (feedbackDetails && feedbackDetails.length > 0) if (feedbackDetails && feedbackDetails.length > 0)
callStats.sendFeedback( Feedback.feedbackScore, APP.conference.sendFeedback( Feedback.feedbackScore,
feedbackDetails); feedbackDetails);
if (feedbackWindowCallback) if (feedbackWindowCallback)

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,17 @@
/* global APP, interfaceConfig, $, Strophe */ /* global APP, interfaceConfig, $ */
/* jshint -W101 */ /* jshint -W101 */
var CanvasUtil = require("./CanvasUtils");
var ASDrawContext = null; import CanvasUtil from './CanvasUtils';
import BottomToolbar from '../toolbars/BottomToolbar';
function initActiveSpeakerAudioLevels() { const LOCAL_LEVEL = 'local';
var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2; let ASDrawContext = null;
let audioLevelCanvasCache = {};
function initDominantSpeakerAudioLevels() {
let ASRadius = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE / 2;
let ASCenter = (interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
// Draw a circle. // Draw a circle.
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI); ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
@ -17,250 +22,214 @@ function initActiveSpeakerAudioLevels() {
ASDrawContext.shadowOffsetY = 0; 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. * The audio Levels plugin.
*/ */
var AudioLevels = (function(my) { const AudioLevels = {
var audioLevelCanvasCache = {};
my.LOCAL_LEVEL = 'local'; init () {
ASDrawContext = $('#dominantSpeakerAudioLevel')[0].getContext('2d');
my.init = function () { initDominantSpeakerAudioLevels();
ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d'); },
initActiveSpeakerAudioLevels();
};
/** /**
* 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. * didn't exist we create it.
*/ */
my.updateAudioLevelCanvas = function (peerJid, VideoLayout) { updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
var resourceJid = null; let videoSpanId = 'localVideoContainer';
var videoSpanId = null; if (id) {
if (!peerJid) videoSpanId = `participant_${id}`;
videoSpanId = 'localVideoContainer';
else {
resourceJid = Strophe.getResourceFromJid(peerJid);
videoSpanId = 'participant_' + resourceJid;
} }
var videoSpan = document.getElementById(videoSpanId); let videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) { if (!videoSpan) {
if (resourceJid) if (id) {
console.error("No video element for jid", resourceJid); console.error("No video element for id", id);
else } else {
console.error("No video element for local video."); console.error("No video element for local video.");
}
return; return;
} }
var audioLevelCanvas = $('#' + videoSpanId + '>canvas'); let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var thumbnailWidth = thumbnailSize[0];
var thumbnailHeight = thumbnailSize[1];
if (!audioLevelCanvas || audioLevelCanvas.length === 0) { if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas'); audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel"; audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px"; audioLevelCanvas.style.bottom = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px"; audioLevelCanvas.style.left = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
resizeAudioLevelCanvas( audioLevelCanvas, resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
thumbnailWidth,
thumbnailHeight);
videoSpan.appendChild(audioLevelCanvas); videoSpan.appendChild(audioLevelCanvas);
} else { } else {
audioLevelCanvas = audioLevelCanvas.get(0); audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas( audioLevelCanvas, resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
thumbnailWidth,
thumbnailHeight);
} }
}; },
/** /**
* 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 * @param id id of the user for whom we draw the audio level
* which we draw the audio level
* @param audioLevel the newAudio level to render * @param audioLevel the newAudio level to render
*/ */
my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) { updateAudioLevel (id, audioLevel, largeVideoId) {
drawAudioLevelCanvas(resourceJid, audioLevel); 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; return;
}
var drawContext = audioLevelCanvas.getContext('2d'); let drawContext = audioLevelCanvas.getContext('2d');
var canvasCache = audioLevelCanvasCache[resourceJid]; let canvasCache = audioLevelCanvasCache[id];
drawContext.clearRect (0, 0, drawContext.clearRect(
audioLevelCanvas.width, audioLevelCanvas.height); 0, 0, audioLevelCanvas.width, audioLevelCanvas.height
);
drawContext.drawImage(canvasCache, 0, 0); drawContext.drawImage(canvasCache, 0, 0);
if(resourceJid === AudioLevels.LOCAL_LEVEL) { if (id === LOCAL_LEVEL) {
if(!APP.xmpp.myJid()) { id = APP.conference.localId;
if (!id) {
return; return;
} }
resourceJid = APP.xmpp.myResource();
} }
if(resourceJid === largeVideoResourceJid) { if(id === largeVideoId) {
window.requestAnimationFrame(function () { window.requestAnimationFrame(function () {
AudioLevels.updateActiveSpeakerAudioLevel(audioLevel); AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
}); });
} }
}; },
my.updateActiveSpeakerAudioLevel = function(audioLevel) { updateDominantSpeakerAudioLevel (audioLevel) {
if($("#activeSpeaker").css("visibility") == "hidden" || ASDrawContext === null) if($("#domiantSpeaker").css("visibility") == "hidden" || ASDrawContext === null) {
return; return;
}
ASDrawContext.clearRect(0, 0, 300, 300); ASDrawContext.clearRect(0, 0, 300, 300);
if (!audioLevel) if (!audioLevel) {
return; return;
}
ASDrawContext.shadowBlur = getShadowLevel(audioLevel); ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
// Fill the shape. // Fill the shape.
ASDrawContext.fill(); ASDrawContext.fill();
}; },
/** updateCanvasSize (thumbWidth, thumbHeight) {
* Resizes the given audio level canvas to match the given thumbnail size. let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
*/ let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
function resizeAudioLevelCanvas(audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
/** BottomToolbar.getThumbs().children('canvas').width(canvasWidth).height(canvasHeight);
* 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]) {
var videoSpanId = getVideoSpanId(resourceJid); Object.keys(audioLevelCanvasCache).forEach(function (id) {
audioLevelCanvasCache[id].width = canvasWidth;
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0); audioLevelCanvasCache[id].height = canvasHeight;
/*
* 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;
}
}); });
}
};
if (resized) export default AudioLevels;
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;

View File

@ -1,7 +1,7 @@
/** /**
* Utility class for drawing canvas shapes. * Utility class for drawing canvas shapes.
*/ */
var CanvasUtil = (function(my) { const CanvasUtil = {
/** /**
* Draws a round rectangle with a glow. The glowWidth indicates the depth * Draws a round rectangle with a glow. The glowWidth indicates the depth
@ -15,8 +15,7 @@ var CanvasUtil = (function(my) {
* @param glowColor the color of the glow * @param glowColor the color of the glow
* @param glowWidth the width of the glow * @param glowWidth the width of the glow
*/ */
my.drawRoundRectGlow drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
= function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context. // Save the previous state of the context.
drawContext.save(); drawContext.save();
@ -73,14 +72,14 @@ var CanvasUtil = (function(my) {
// Restore the previous context state. // Restore the previous context state.
drawContext.restore(); drawContext.restore();
}; },
/** /**
* Clones the given canvas. * Clones the given canvas.
* *
* @return the new cloned canvas. * @return the new cloned canvas.
*/ */
my.cloneCanvas = function (oldCanvas) { cloneCanvas (oldCanvas) {
/* /*
* FIXME Testing has shown that oldCanvas may not exist. In such a case, * FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio * the method CanvasUtil.cloneCanvas may throw an error. Since audio
@ -103,9 +102,7 @@ var CanvasUtil = (function(my) {
//return the new canvas //return the new canvas
return newCanvas; return newCanvas;
}; }
};
return my; export default CanvasUtil;
})(CanvasUtil || {});
module.exports = CanvasUtil;

View File

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

View File

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

View File

@ -1,75 +1,102 @@
/* global $, APP, config*/ /* global $, APP, config*/
var XMPP = require('../../xmpp/xmpp'); var messageHandler = require('../util/MessageHandler');
var Moderator = require('../../xmpp/moderator');
//FIXME: use LoginDialog to add retries to XMPP.connect method used when
// anonymous domain is not enabled
/** /**
* Creates new <tt>Dialog</tt> instance. * Build html for "password required" dialog.
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt> called * @returns {string} html string
* when we either fail to connect or succeed(check Strophe.Status).
* @param obtainSession <tt>true</tt> if we want to send ConferenceIQ to Jicofo
* in order to create session-id after the connection is established.
* @constructor
*/ */
function Dialog(callback, obtainSession) { function getPasswordInputHtml() {
let placeholder = config.hosts.authdomain
? "user identity"
: "user@domain.net";
let passRequiredMsg = APP.translation.translateString(
"dialog.passwordRequired"
);
return `
<h2 data-i18n="dialog.passwordRequired">${passRequiredMsg}</h2>
<input name="username" type="text" placeholder=${placeholder} autofocus>
<input name="password" type="password"
data-i18n="[placeholder]dialog.userPassword"
placeholder="user password">
`;
}
var self = this; /**
* Convert provided id to jid if it's not jid yet.
var stop = false; * @param {string} id user id or jid
* @returns {string} jid
var connection = APP.xmpp.createConnection(); */
function toJid(id) {
var message = '<h2 data-i18n="dialog.passwordRequired">'; if (id.indexOf("@") >= 0) {
message += APP.translation.translateString("dialog.passwordRequired"); return id;
message += '</h2>' +
'<input name="username" type="text" ';
if (config.hosts.authdomain) {
message += 'placeholder="user identity" autofocus>';
} else {
message += 'placeholder="user@domain.net" autofocus>';
} }
message += '<input name="password" ' +
'type="password" data-i18n="[placeholder]dialog.userPassword"' +
' placeholder="user password">';
var okButton = APP.translation.generateTranslationHTML("dialog.Ok"); let jid = id.concat('@');
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: { login: {
html: message, html: getPasswordInputHtml(),
buttons: [ buttons: loginButtons,
{ title: okButton, value: true},
{ title: cancelButton, value: false}
],
focus: ':input:first', focus: ':input:first',
submit: function (e, v, m, f) { submit: function (e, v, m, f) {
e.preventDefault(); e.preventDefault();
if (v) { if (v) {
var jid = f.username; let jid = f.username;
var password = f.password; let password = f.password;
if (jid && 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'); connDialog.goToState('connecting');
connection.connect(jid, password, stateHandler); successCallback(toJid(jid), password);
} }
} else { } else {
// User cancelled // User cancelled
stop = true; cancelCallback();
callback();
} }
} }
}, },
@ -82,115 +109,23 @@ function Dialog(callback, obtainSession) {
finished: { finished: {
title: APP.translation.translateString('dialog.error'), title: APP.translation.translateString('dialog.error'),
html: '<div id="errorMessage"></div>', html: '<div id="errorMessage"></div>',
buttons: [ buttons: finishedButtons,
{
title: APP.translation.translateString('dialog.retry'),
value: 'retry'
},
{
title: APP.translation.translateString('dialog.Cancel'),
value: 'cancel'
},
],
defaultButton: 0, defaultButton: 0,
submit: function (e, v, m, f) { submit: function (e, v, m, f) {
e.preventDefault(); e.preventDefault();
if (v === 'retry') if (v === 'retry') {
connDialog.goToState('login'); connDialog.goToState('login');
else } else {
callback(); // User cancelled
cancelCallback();
}
} }
} }
}; };
var connDialog var connDialog = messageHandler.openDialogWithStates(
= APP.UI.messageHandler.openDialogWithStates(states, states, { persistent: true, closeText: '' }, null
{ persistent: true, closeText: '' }, null); );
var stateHandler = function (status, message) {
if (stop) {
return;
}
var translateKey = "connection." + XMPP.getStatusString(status);
var statusStr = APP.translation.translateString(translateKey);
// Display current state
var connectionStatus =
connDialog.getState('connecting').find('#connectionStatus');
connectionStatus.text(statusStr);
switch (status) {
case XMPP.Status.CONNECTED:
stop = true;
if (!obtainSession) {
callback(connection, status);
return;
}
// Obtaining session-id status
connectionStatus.text(
APP.translation.translateString(
'connection.FETCH_SESSION_ID'));
// Authenticate with Jicofo and obtain session-id
var roomName = APP.UI.generateRoomName();
// Jicofo will return new session-id when connected
// from authenticated domain
connection.sendIQ(
Moderator.createConferenceIq(roomName),
function (result) {
connectionStatus.text(
APP.translation.translateString(
'connection.GOT_SESSION_ID'));
stop = true;
// Parse session-id
Moderator.parseSessionId(result);
callback(connection, status);
},
function (error) {
console.error("Auth on the fly failed", error);
stop = true;
var errorMsg =
APP.translation.translateString(
'connection.GET_SESSION_ID_ERROR') +
$(error).find('>error').attr('code');
self.displayError(errorMsg);
connection.disconnect();
});
break;
case XMPP.Status.AUTHFAIL:
case XMPP.Status.CONNFAIL:
case XMPP.Status.DISCONNECTED:
stop = true;
callback(connection, status);
var errorMessage = statusStr;
if (message)
{
errorMessage += ': ' + message;
}
self.displayError(errorMessage);
break;
default:
break;
}
};
/** /**
* Displays error message in 'finished' state which allows either to cancel * Displays error message in 'finished' state which allows either to cancel
@ -199,42 +134,103 @@ function Dialog(callback, obtainSession) {
*/ */
this.displayError = function (message) { this.displayError = function (message) {
var finishedState = connDialog.getState('finished'); let finishedState = connDialog.getState('finished');
var errorMessageElem = finishedState.find('#errorMessage'); let errorMessageElem = finishedState.find('#errorMessage');
errorMessageElem.text(message); errorMessageElem.text(message);
connDialog.goToState('finished'); connDialog.goToState('finished');
}; };
/**
* Show message as connection status.
* @param {string} message
*/
this.displayConnectionStatus = function (message) {
let connectingState = connDialog.getState('connecting');
let connectionStatus = connectingState.find('#connectionStatus');
connectionStatus.text(message);
};
/** /**
* Closes LoginDialog. * Closes LoginDialog.
*/ */
this.close = function () { this.close = function () {
stop = true;
connDialog.close(); connDialog.close();
}; };
} }
var LoginDialog = { export default {
/** /**
* Displays login prompt used to establish new XMPP connection. Given * Show new auth dialog for JitsiConnection.
* <tt>callback(Strophe.Connection, Strophe.Status)</tt> function will be *
* called when we connect successfully(status === CONNECTED) or when we fail * @param {function(jid, password)} successCallback
* to do so. On connection failure program can call Dialog.close() method in * @param {function} [cancelCallback] callback to invoke if user canceled.
* order to cancel or do nothing to let the user retry. *
* @param callback <tt>function(Strophe.Connection, Strophe.Status)</tt> * @returns {LoginDialog}
* called when we either fail to connect or succeed(check
* Strophe.Status).
* @param obtainSession <tt>true</tt> if we want to send ConferenceIQ to
* Jicofo in order to create session-id after the connection is
* established.
* @returns {Dialog}
*/ */
show: function (callback, obtainSession) { showAuthDialog: function (successCallback, cancelCallback) {
return new Dialog(callback, obtainSession); return new LoginDialog(successCallback, cancelCallback);
},
/**
* Show notification that external auth is required (using provided url).
* @param {string} url URL to use for external auth.
* @param {function} callback callback to invoke when auth popup is closed.
* @returns auth dialog
*/
showExternalAuthDialog: function (url, callback) {
var dialog = messageHandler.openCenteredPopup(
url, 910, 660,
// On closed
callback
);
if (!dialog) {
messageHandler.openMessageDialog(null, "dialog.popupError");
}
return dialog;
},
/**
* Show notification that authentication is required
* to create the conference, so he should authenticate or wait for a host.
* @param {string} roomName name of the conference
* @param {function} onAuthNow callback to invoke if
* user want to authenticate.
* @returns dialog
*/
showAuthRequiredDialog: function (roomName, onAuthNow) {
var title = APP.translation.generateTranslationHTML(
"dialog.WaitingForHost"
);
var msg = APP.translation.generateTranslationHTML(
"dialog.WaitForHostMsg", {room: roomName}
);
var buttonTxt = APP.translation.generateTranslationHTML(
"dialog.IamHost"
);
var buttons = [{title: buttonTxt, value: "authNow"}];
return APP.UI.messageHandler.openDialog(
title,
msg,
true,
buttons,
function (e, submitValue) {
// Do not close the dialog yet
e.preventDefault();
// Open login popup
if (submitValue === 'authNow') {
onAuthNow();
}
}
);
} }
}; };
module.exports = LoginDialog;

View File

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

View File

@ -1,68 +1,62 @@
/* global Strophe, APP, MD5, config, interfaceConfig */ /* global MD5, config, interfaceConfig */
var Settings = require("../../settings/Settings");
var users = {}; let users = {};
var Avatar = { export default {
/** /**
* Sets the user's avatar in the settings menu(if local user), contact list * Sets the user's avatar in the settings menu(if local user), contact list
* and thumbnail * and thumbnail
* @param jid jid of the user * @param id id of the user
* @param id email or userID to be used as a hash * @param email email or nickname to be used as a hash
*/ */
setUserAvatar: function (jid, id) { setUserAvatar: function (id, email) {
if (id) { if (email) {
if (users[jid] === id) { if (users[id] === email) {
return; return;
} }
users[jid] = id; users[id] = email;
} }
var avatarUrl = this.getAvatarUrl(jid);
var resourceJid = Strophe.getResourceFromJid(jid);
APP.UI.userAvatarChanged(resourceJid, avatarUrl);
}, },
/** /**
* Returns the URL of the image for the avatar of a particular user, * Returns the URL of the image for the avatar of a particular user,
* identified by its jid * identified by its id.
* @param jid * @param {string} userId user id
* @param jid full MUC jid of the user for whom we want to obtain avatar URL
*/ */
getAvatarUrl: function (jid) { getAvatarUrl: function (userId) {
if (config.disableThirdPartyRequests) { if (config.disableThirdPartyRequests) {
return 'images/avatar2.png'; return 'images/avatar2.png';
} else {
if (!jid) {
console.error("Get avatar - jid is undefined");
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;

View File

@ -1,61 +1,20 @@
/* global $, config, /* global $ */
setLargeVideoVisible, Util */
var VideoLayout = require("../videolayout/VideoLayout");
var Prezi = require("../prezi/Prezi");
var UIUtil = require("../util/UIUtil");
var etherpadName = null;
var etherpadIFrame = null;
var domain = null;
var options = "?showControls=true&showChat=false&showLineNumbers=true" +
"&useMonospaceFont=false";
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import BottomToolbar from '../toolbars/BottomToolbar';
/** /**
* Resizes the etherpad. * Etherpad options.
*/ */
function resize() { const options = $.param({
if ($('#etherpad>iframe').length) { showControns: true,
var remoteVideos = $('#remoteVideos'); showChat: false,
var availableHeight showLineNumbers: true,
= window.innerHeight - remoteVideos.outerHeight(); useMonospaceFont: false
var availableWidth = UIUtil.getAvailableVideoWidth(); });
$('#etherpad>iframe').width(availableWidth);
$('#etherpad>iframe').height(availableHeight);
}
}
/**
* Creates the Etherpad button and adds it to the toolbar.
*/
function enableEtherpadButton() {
if (!$('#toolbar_button_etherpad').is(":visible"))
$('#toolbar_button_etherpad').css({display: 'inline-block'});
}
/**
* Creates the IFrame for the etherpad.
*/
function createIFrame() {
etherpadIFrame = VideoLayout.createEtherpadIframe(
domain + etherpadName + options, function() {
document.domain = document.domain;
bubbleIframeMouseMove(etherpadIFrame);
setTimeout(function() {
// the iframes inside of the etherpad are
// not yet loaded when the etherpad iframe is loaded
var outer = etherpadIFrame.
contentDocument.getElementsByName("ace_outer")[0];
bubbleIframeMouseMove(outer);
var inner = outer.
contentDocument.getElementsByName("ace_inner")[0];
bubbleIframeMouseMove(inner);
}, 2000);
});
}
function bubbleIframeMouseMove(iframe){ function bubbleIframeMouseMove(iframe){
var existingOnMouseMove = iframe.contentWindow.onmousemove; var existingOnMouseMove = iframe.contentWindow.onmousemove;
@ -71,8 +30,8 @@ function bubbleIframeMouseMove(iframe){
e.detail, e.detail,
e.screenX, e.screenX,
e.screenY, e.screenY,
e.clientX + boundingClientRect.left, e.clientX + boundingClientRect.left,
e.clientY + boundingClientRect.top, e.clientY + boundingClientRect.top,
e.ctrlKey, e.ctrlKey,
e.altKey, e.altKey,
e.shiftKey, 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 = { const EtherpadContainerType = "etherpad";
/**
* Initializes the etherpad.
*/
init: function (name) {
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);
/** iframe.onload = function() {
* Resizes the etherpad, when the window is resized. document.domain = document.domain;
*/ bubbleIframeMouseMove(iframe);
$(window).resize(function () {
resize();
});
}
},
/** setTimeout(function() {
* Opens/hides the Etherpad. const doc = iframe.contentDocument;
*/
toggleEtherpad: function (isPresentation) {
if (!etherpadIFrame)
createIFrame();
// 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") const inner = doc.getElementsByName("ace_inner")[0];
{ bubbleIframeMouseMove(inner);
VideoLayout.setLargeVideoState("video"); }, 2000);
} };
else
{ this.iframe = iframe;
VideoLayout.setLargeVideoState("etherpad");
}
resize();
} }
};
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);
}
}

View File

@ -1,343 +1,448 @@
var ToolbarToggler = require("../toolbars/ToolbarToggler"); /* global $, APP */
var UIUtil = require("../util/UIUtil"); /* jshint -W101 */
var VideoLayout = require("../videolayout/VideoLayout");
var messageHandler = require("../util/MessageHandler");
var PreziPlayer = require("./PreziPlayer");
var preziPlayer = null;
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import PreziPlayer from './PreziPlayer';
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import messageHandler from '../util/MessageHandler';
import ToolbarToggler from "../toolbars/ToolbarToggler";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import BottomToolbar from '../toolbars/BottomToolbar';
/** /**
* Shows/hides a presentation. * Example of Prezi link.
*/ */
function setPresentationVisible(visible) { const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi";
const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i;
if (visible) { /**
VideoLayout.setLargeVideoState("prezi"); * Default aspect ratio for Prezi frame.
} */
else { const aspectRatio = 16.0 / 9.0;
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);
}
}
};
/** /**
* A new presentation has been added. * Default Prezi frame width.
*
* @param event the event indicating the add of a presentation
* @param jid the jid from which the presentation was added
* @param presUrl url of the presentation
* @param currentSlide the current slide to which we should move
*/ */
function presentationAdded(event, jid, presUrl, currentSlide) { const DEFAULT_WIDTH = 640;
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);
}
);
};
/** /**
* A presentation has been removed. * Default Prezi frame height.
*
* @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
*/ */
function presentationRemoved(event, jid, presUrl) { const DEFAULT_HEIGHT = 480;
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;
}
};
/** /**
* Indicates if the given string is an alphanumeric string. * Indicates if the given string is an alphanumeric string.
* Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the * Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
* purpose of checking URIs. * purpose of checking URIs.
* @param {string} unsafeText string to check
* @returns {boolean}
*/ */
function isAlphanumeric(unsafeText) { function isAlphanumeric(unsafeText) {
var regex = /^[a-z0-9-_\/&\?=;]+$/i; return alphanumRegex.test(unsafeText);
return regex.test(unsafeText);
} }
/** /**
* Returns the presentation id from the given url. * Returns the presentation id from the given url.
* @param {string} url Prezi link
* @returns {string} presentation id
*/ */
function getPresentationId (presUrl) { function getPresentationId (url) {
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10); let presId = url.substring(url.indexOf("prezi.com/") + 10);
return presIdTmp.substring(0, presIdTmp.indexOf('/')); 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() { function isPreziLink(url) {
var availableWidth = UIUtil.getAvailableVideoWidth(); if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
var availableHeight = getPresentationHeihgt(); return false;
var aspectRatio = 16.0 / 9.0;
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
} }
return availableWidth;
}
/** let presId = url.substring(url.indexOf("prezi.com/") + 10);
* Returns the presentation height. if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
*/ return false;
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());
} }
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) { function requestPreziLink() {
if (preziPlayer && preziPlayer.getCurrentStep() != current) { const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle");
preziPlayer.flyToStep(current); 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(); return new Promise(function (resolve, reject) {
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) { let dialog = messageHandler.openDialogWithStates({
preziPlayer.flyToStep(current, i); 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;
});
}
}

View File

@ -1,298 +1,290 @@
/* global PreziPlayer */
/* jshint -W101 */ /* jshint -W101 */
(function() {
"use strict";
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
window.PreziPlayer = (function() { var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
PreziPlayer.API_VERSION = 1; PreziPlayer.API_VERSION = 1;
PreziPlayer.CURRENT_STEP = 'currentStep'; PreziPlayer.CURRENT_STEP = 'currentStep';
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep'; PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
PreziPlayer.CURRENT_OBJECT = 'currentObject'; PreziPlayer.CURRENT_OBJECT = 'currentObject';
PreziPlayer.STATUS_LOADING = 'loading'; PreziPlayer.STATUS_LOADING = 'loading';
PreziPlayer.STATUS_READY = 'ready'; PreziPlayer.STATUS_READY = 'ready';
PreziPlayer.STATUS_CONTENT_READY = 'contentready'; PreziPlayer.STATUS_CONTENT_READY = 'contentready';
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange"; PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange"; PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange"; PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
PreziPlayer.EVENT_STATUS = "statusChange"; PreziPlayer.EVENT_STATUS = "statusChange";
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange"; PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
PreziPlayer.EVENT_IS_MOVING = "isMovingChange"; PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
PreziPlayer.domain = "https://prezi.com"; PreziPlayer.domain = "https://prezi.com";
PreziPlayer.path = "/player/"; PreziPlayer.path = "/player/";
PreziPlayer.players = {}; PreziPlayer.players = {};
PreziPlayer.binded_methods = ['changesHandler']; PreziPlayer.binded_methods = ['changesHandler'];
PreziPlayer.createMultiplePlayers = function(optionArray){ PreziPlayer.createMultiplePlayers = function(optionArray){
for(var i=0; i<optionArray.length; i++) { for(var i=0; i<optionArray.length; i++) {
var optionSet = optionArray[i]; var optionSet = optionArray[i];
new PreziPlayer(optionSet.id, optionSet); new PreziPlayer(optionSet.id, optionSet);
}
};
PreziPlayer.messageReceived = function(event){
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);
PreziPlayer.messageReceived = function(event){ }
var message, item, player; for (var i = 0; i < player.callbacks.length; i++) {
try { item = player.callbacks[i];
message = JSON.parse(event.data); if (item && message.type === item.event) {
if (message.id && (player = PreziPlayer.players[message.id])) { item.callback(message);
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);
}
}
} }
} catch (e) { } }
}; }
} catch (e) { }
};
/*jshint -W004 */ /*jshint -W004 */
function PreziPlayer(id, options) { function PreziPlayer(id, options) {
/*jshint +W004 */ /*jshint +W004 */
var params, paramString = "", _this = this; var params, paramString = "", _this = this;
if (PreziPlayer.players[id]){ if (PreziPlayer.players[id]){
PreziPlayer.players[id].destroy(); PreziPlayer.players[id].destroy();
} }
for(var i=0; i<PreziPlayer.binded_methods.length; i++) { for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
var method_name = PreziPlayer.binded_methods[i]; var method_name = PreziPlayer.binded_methods[i];
_this[method_name] = __bind(_this[method_name], _this); _this[method_name] = __bind(_this[method_name], _this);
} }
options = options || {}; options = options || {};
this.options = options; this.options = options;
this.values = {'status': PreziPlayer.STATUS_LOADING}; this.values = {'status': PreziPlayer.STATUS_LOADING};
this.values[PreziPlayer.CURRENT_STEP] = 0; this.values[PreziPlayer.CURRENT_STEP] = 0;
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0; this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
this.values[PreziPlayer.CURRENT_OBJECT] = null; this.values[PreziPlayer.CURRENT_OBJECT] = null;
this.callbacks = []; this.callbacks = [];
this.id = id; this.id = id;
this.embedTo = document.getElementById(id); this.embedTo = document.getElementById(id);
if (!this.embedTo) { if (!this.embedTo) {
throw "The element id is not available."; throw "The element id is not available.";
} }
this.iframe = document.createElement('iframe'); this.iframe = document.createElement('iframe');
params = [ params = [
{ name: 'oid', value: options.preziId }, { name: 'oid', value: options.preziId },
{ name: 'explorable', value: options.explorable ? 1 : 0 }, { name: 'explorable', value: options.explorable ? 1 : 0 },
{ name: 'controls', value: options.controls ? 1 : 0 } { name: 'controls', value: options.controls ? 1 : 0 }
]; ];
for (i=0; i<params.length; i++) { for (i=0; i<params.length; i++) {
var param = params[i]; var param = params[i];
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value; paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
} }
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString; this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
this.iframe.frameBorder = 0; this.iframe.frameBorder = 0;
this.iframe.scrolling = "no"; this.iframe.scrolling = "no";
this.iframe.width = options.width || 640; this.iframe.width = options.width || 640;
this.iframe.height = options.height || 480; this.iframe.height = options.height || 480;
this.embedTo.innerHTML = ''; this.embedTo.innerHTML = '';
// JITSI: IN CASE SOMETHING GOES WRONG. // JITSI: IN CASE SOMETHING GOES WRONG.
try { try {
this.embedTo.appendChild(this.iframe); this.embedTo.appendChild(this.iframe);
} }
catch (err) { catch (err) {
console.log("CATCH ERROR"); console.log("CATCH ERROR");
} }
// JITSI: Increase interval from 200 to 500, which fixes prezi // JITSI: Increase interval from 200 to 500, which fixes prezi
// crashes for us. // crashes for us.
this.initPollInterval = setInterval(function(){ this.initPollInterval = setInterval(function(){
_this.sendMessage({'action': 'init'}); _this.sendMessage({'action': 'init'});
}, 500); }, 500);
PreziPlayer.players[id] = this; PreziPlayer.players[id] = this;
} }
PreziPlayer.prototype.changesHandler = function(message) { PreziPlayer.prototype.changesHandler = function(message) {
var key, value, j, item; var key, value, j, item;
if (this.initPollInterval) { if (this.initPollInterval) {
clearInterval(this.initPollInterval); clearInterval(this.initPollInterval);
this.initPollInterval = false; this.initPollInterval = false;
} }
for (key in message.data) { for (key in message.data) {
if (message.data.hasOwnProperty(key)){ if (message.data.hasOwnProperty(key)){
value = message.data[key]; value = message.data[key];
this.values[key] = value; this.values[key] = value;
for (j=0; j<this.callbacks.length; j++) { 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--) {
item = this.callbacks[j]; item = this.callbacks[j];
if (item && item.event === event && (callback === undefined || item.callback === callback)){ if (item && item.event === key + "Change"){
this.callbacks.splice(j, 1); 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;

View File

@ -1,102 +1,100 @@
/* global require, $ */ /* global require, $ */
var Chat = require("./chat/Chat"); import Chat from "./chat/Chat";
var ContactList = require("./contactlist/ContactList"); import ContactList from "./contactlist/ContactList";
var Settings = require("./../../settings/Settings"); import Settings from "./../../settings/Settings";
var SettingsMenu = require("./settings/SettingsMenu"); import SettingsMenu from "./settings/SettingsMenu";
var VideoLayout = require("../videolayout/VideoLayout"); import VideoLayout from "../videolayout/VideoLayout";
var ToolbarToggler = require("../toolbars/ToolbarToggler"); import ToolbarToggler from "../toolbars/ToolbarToggler";
var UIUtil = require("../util/UIUtil"); import UIUtil from "../util/UIUtil";
var LargeVideo = require("../videolayout/LargeVideo");
const buttons = {
'#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.. * Toggler for the chat, contact list, settings menu, etc..
*/ */
var PanelToggler = (function(my) { var PanelToggler = {
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;
}
};
/** /**
* Opens / closes the chat area. * Opens / closes the chat area.
*/ */
my.toggleChat = function() { toggleChat () {
var chatCompleteFunction = Chat.isVisible() ? var chatCompleteFunction = Chat.isVisible()
function() {} : function () { ? function () {}
Chat.scrollChatToBottom(); : function () {
$('#chatspace').trigger('shown'); Chat.scrollChatToBottom();
}; $('#chatspace').trigger('shown');
};
VideoLayout.resizeVideoArea(!Chat.isVisible(), chatCompleteFunction); VideoLayout.resizeVideoArea(!Chat.isVisible(), chatCompleteFunction);
@ -112,16 +110,24 @@ var PanelToggler = (function(my) {
} }
}, },
null, null,
Chat.resizeChat, () => this.resizeChat(),
null); null);
}; },
resizeChat () {
let [width, height] = UIUtil.getSidePanelSize();
Chat.resizeChat(width, height);
},
/** /**
* Opens / closes the contact list area. * Opens / closes the contact list area.
*/ */
my.toggleContactList = function () { toggleContactList () {
var completeFunction = ContactList.isVisible() ? var completeFunction = ContactList.isVisible()
function() {} : function () { $('#contactlist').trigger('shown');}; ? function () {}
: function () {
$('#contactlist').trigger('shown');
};
VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction); VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
toggle(ContactList, toggle(ContactList,
@ -131,12 +137,12 @@ var PanelToggler = (function(my) {
ContactList.setVisualNotification(false); ContactList.setVisualNotification(false);
}, },
null); null);
}; },
/** /**
* Opens / closes the settings menu * Opens / closes the settings menu
*/ */
my.toggleSettingsMenu = function() { toggleSettingsMenu () {
VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){}); VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){});
toggle(SettingsMenu, toggle(SettingsMenu,
'#settingsmenu', '#settingsmenu',
@ -147,31 +153,13 @@ var PanelToggler = (function(my) {
$('#setEmail').get(0).value = settings.email; $('#setEmail').get(0).value = settings.email;
}, },
null); null);
}; },
/** isVisible () {
* Returns the size of the side panel.
*/
my.getPanelSize = function () {
var availableHeight = window.innerHeight;
var availableWidth = window.innerWidth;
var panelWidth = 200;
if (availableWidth * 0.2 < 200) {
panelWidth = availableWidth * 0.2;
}
return [panelWidth, availableHeight];
};
my.isVisible = function() {
return (Chat.isVisible() || return (Chat.isVisible() ||
ContactList.isVisible() || ContactList.isVisible() ||
SettingsMenu.isVisible()); SettingsMenu.isVisible());
}; }
};
return my; export default PanelToggler;
}(PanelToggler || {}));
module.exports = PanelToggler;

View File

@ -1,11 +1,13 @@
/* global APP, $, Util, nickname:true */ /* global APP, $ */
var Replacement = require("./Replacement");
var CommandsProcessor = require("./Commands"); import {processReplacements, linkify} from './Replacement';
var ToolbarToggler = require("../../toolbars/ToolbarToggler"); import CommandsProcessor from './Commands';
import ToolbarToggler from '../../toolbars/ToolbarToggler';
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
var smileys = require("./smileys.json").smileys; var smileys = require("./smileys.json").smileys;
var NicknameHandler = require("../../util/NicknameHandler");
var UIUtil = require("../../util/UIUtil");
var UIEvents = require("../../../../service/UI/UIEvents");
var notificationInterval = false; var notificationInterval = false;
var unreadMessages = 0; var unreadMessages = 0;
@ -165,28 +167,21 @@ function resizeChatConversation() {
/** /**
* Chat related user interface. * Chat related user interface.
*/ */
var Chat = (function (my) { var Chat = {
/** /**
* Initializes chat related interface. * Initializes chat related interface.
*/ */
my.init = function () { init (eventEmitter) {
if(NicknameHandler.getNickname()) if (APP.settings.getDisplayName()) {
Chat.setChatConversationMode(true); Chat.setChatConversationMode(true);
NicknameHandler.addListener(UIEvents.NICKNAME_CHANGED, }
function (nickname) {
Chat.setChatConversationMode(true);
});
$('#nickinput').keydown(function (event) { $('#nickinput').keydown(function (event) {
if (event.keyCode === 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
var val = UIUtil.escapeHtml(this.value); var val = UIUtil.escapeHtml(this.value);
this.value = ''; this.value = '';
if (!NicknameHandler.getNickname()) { eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val);
NicknameHandler.setNickname(val);
return;
}
} }
}); });
@ -197,14 +192,12 @@ var Chat = (function (my) {
var value = this.value; var value = this.value;
usermsg.val('').trigger('autosize.resize'); usermsg.val('').trigger('autosize.resize');
this.focus(); this.focus();
var command = new CommandsProcessor(value); var command = new CommandsProcessor(value, eventEmitter);
if(command.isCommand()) { if (command.isCommand()) {
command.processCommand(); command.processCommand();
} } else {
else {
var message = UIUtil.escapeHtml(value); var message = UIUtil.escapeHtml(value);
APP.xmpp.sendChatMessage(message, eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
NicknameHandler.getNickname());
} }
} }
}); });
@ -222,19 +215,17 @@ var Chat = (function (my) {
}); });
addSmileys(); addSmileys();
}; },
/** /**
* Appends the given message to the chat conversation. * Appends the given message to the chat conversation.
*/ */
my.updateChatConversation = updateChatConversation (id, displayName, message, stamp) {
function (from, displayName, message, myjid, stamp) {
var divClassName = ''; var divClassName = '';
if (APP.xmpp.myJid() === from) { if (APP.conference.isLocalId(id)) {
divClassName = "localuser"; divClassName = "localuser";
} } else {
else {
divClassName = "remoteuser"; divClassName = "remoteuser";
if (!Chat.isVisible()) { if (!Chat.isVisible()) {
@ -250,7 +241,7 @@ var Chat = (function (my) {
var escMessage = message.replace(/</g, '&lt;'). var escMessage = message.replace(/</g, '&lt;').
replace(/>/g, '&gt;').replace(/\n/g, '<br/>'); replace(/>/g, '&gt;').replace(/\n/g, '<br/>');
var escDisplayName = UIUtil.escapeHtml(displayName); var escDisplayName = UIUtil.escapeHtml(displayName);
message = Replacement.processReplacements(escMessage); message = processReplacements(escMessage);
var messageContainer = var messageContainer =
'<div class="chatmessage">'+ '<div class="chatmessage">'+
@ -263,14 +254,14 @@ var Chat = (function (my) {
$('#chatconversation').append(messageContainer); $('#chatconversation').append(messageContainer);
$('#chatconversation').animate( $('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
}; },
/** /**
* Appends error message to the conversation * Appends error message to the conversation
* @param errorMessage the received error message. * @param errorMessage the received error message.
* @param originalText the original message. * @param originalText the original message.
*/ */
my.chatAddError = function(errorMessage, originalText) { chatAddError (errorMessage, originalText) {
errorMessage = UIUtil.escapeHtml(errorMessage); errorMessage = UIUtil.escapeHtml(errorMessage);
originalText = UIUtil.escapeHtml(originalText); originalText = UIUtil.escapeHtml(originalText);
@ -281,28 +272,28 @@ var Chat = (function (my) {
(errorMessage? (' Reason: ' + errorMessage) : '') + '</div>'); (errorMessage? (' Reason: ' + errorMessage) : '') + '</div>');
$('#chatconversation').animate( $('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
}; },
/** /**
* Sets the subject to the UI * Sets the subject to the UI
* @param subject the subject * @param subject the subject
*/ */
my.chatSetSubject = function(subject) { setSubject (subject) {
if (subject) if (subject) {
subject = subject.trim(); subject = subject.trim();
$('#subject').html(Replacement.linkify(UIUtil.escapeHtml(subject))); }
if(subject === "") { $('#subject').html(linkify(UIUtil.escapeHtml(subject)));
if (subject) {
$("#subject").css({display: "block"});
} else {
$("#subject").css({display: "none"}); $("#subject").css({display: "none"});
} }
else { },
$("#subject").css({display: "block"});
}
};
/** /**
* Sets the chat conversation mode. * Sets the chat conversation mode.
*/ */
my.setChatConversationMode = function (isConversationMode) { setChatConversationMode (isConversationMode) {
if (isConversationMode) { if (isConversationMode) {
$('#nickname').css({visibility: 'hidden'}); $('#nickname').css({visibility: 'hidden'});
$('#chatconversation').css({visibility: 'visible'}); $('#chatconversation').css({visibility: 'visible'});
@ -310,42 +301,37 @@ var Chat = (function (my) {
$('#smileysarea').css({visibility: 'visible'}); $('#smileysarea').css({visibility: 'visible'});
$('#usermsg').focus(); $('#usermsg').focus();
} }
}; },
/** /**
* Resizes the chat area. * Resizes the chat area.
*/ */
my.resizeChat = function () { resizeChat (width, height) {
var chatSize = require("../SidePanelToggler").getPanelSize(); $('#chatspace').width(width).height(height);
$('#chatspace').width(chatSize[0]);
$('#chatspace').height(chatSize[1]);
resizeChatConversation(); resizeChatConversation();
}; },
/** /**
* Indicates if the chat is currently visible. * Indicates if the chat is currently visible.
*/ */
my.isVisible = function () { isVisible () {
return $('#chatspace').is(":visible"); return $('#chatspace').is(":visible");
}; },
/** /**
* Shows and hides the window with the smileys * Shows and hides the window with the smileys
*/ */
my.toggleSmileys = toggleSmileys; toggleSmileys,
/** /**
* Scrolls chat to the bottom. * Scrolls chat to the bottom.
*/ */
my.scrollChatToBottom = function() { scrollChatToBottom () {
setTimeout(function () { setTimeout(function () {
$('#chatconversation').scrollTop( $('#chatconversation').scrollTop(
$('#chatconversation')[0].scrollHeight); $('#chatconversation')[0].scrollHeight);
}, 5); }, 5);
}; }
};
export default Chat;
return my;
}(Chat || {}));
module.exports = Chat;

View File

@ -1,12 +1,13 @@
/* global APP, require */ /* global APP */
var UIUtil = require("../../util/UIUtil"); import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
/** /**
* List with supported commands. The keys are the names of the commands and * List with supported commands. The keys are the names of the commands and
* the value is the function that processes the message. * the value is the function that processes the message.
* @type {{String: function}} * @type {{String: function}}
*/ */
var commands = { const commands = {
"topic" : processTopic "topic" : processTopic
}; };
@ -29,9 +30,9 @@ function getCommand(message) {
* Processes the data for topic command. * Processes the data for topic command.
* @param commandArguments the arguments of the topic command. * @param commandArguments the arguments of the topic command.
*/ */
function processTopic(commandArguments) { function processTopic(commandArguments, emitter) {
var topic = UIUtil.escapeHtml(commandArguments); var topic = UIUtil.escapeHtml(commandArguments);
APP.xmpp.setSubject(topic); emitter.emit(UIEvents.SUBJECT_CHANGED, topic);
} }
/** /**
@ -40,9 +41,11 @@ function processTopic(commandArguments) {
* @param message the message * @param message the message
* @constructor * @constructor
*/ */
function CommandsProcessor(message) { function CommandsProcessor(message, emitter) {
var command = getCommand(message); var command = getCommand(message);
this.emitter = emitter;
/** /**
* Returns the name of the command. * Returns the name of the command.
* @returns {String} the command * @returns {String} the command
@ -80,7 +83,7 @@ CommandsProcessor.prototype.processCommand = function() {
if(!this.isCommand()) if(!this.isCommand())
return; return;
commands[this.getCommand()](this.getArgument()); commands[this.getCommand()](this.getArgument(), this.emitter);
}; };
module.exports = CommandsProcessor; export default CommandsProcessor;

View File

@ -1,10 +1,10 @@
/* jshint -W101 */ /* jshint -W101 */
var Smileys = require("./smileys.json"); var Smileys = require("./smileys.json");
/** /**
* Processes links and smileys in "body" * Processes links and smileys in "body"
*/ */
function processReplacements(body) export function processReplacements(body) {
{
//make links clickable //make links clickable
body = linkify(body); body = linkify(body);
@ -18,8 +18,7 @@ function processReplacements(body)
* Finds and replaces all links in the links in "body" * Finds and replaces all links in the links in "body"
* with their <a href=""></a> * with their <a href=""></a>
*/ */
function linkify(inputText) export function linkify(inputText) {
{
var replacedText, replacePattern1, replacePattern2, replacePattern3; var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp:// //URLs starting with http://, https://, or ftp://
@ -40,8 +39,7 @@ function linkify(inputText)
/** /**
* Replaces common smiley strings with images * Replaces common smiley strings with images
*/ */
function smilify(body) function smilify(body) {
{
if(!body) { if(!body) {
return body; return body;
} }
@ -56,8 +54,3 @@ function smilify(body)
return body; return body;
} }
module.exports = {
processReplacements: processReplacements,
linkify: linkify
};

View File

@ -1,8 +1,9 @@
/* global $, APP, Strophe */ /* global $, APP */
var Avatar = require('../../avatar/Avatar'); import Avatar from '../../avatar/Avatar';
import UIEvents from '../../../../service/UI/UIEvents';
var numberOfContacts = 0; let numberOfContacts = 0;
var notificationInterval; let notificationInterval;
/** /**
* Updates the number of participants in the contact list button and sets * Updates the number of participants in the contact list button and sets
@ -30,7 +31,7 @@ function updateNumberOfParticipants(delta) {
* @return {object} the newly created avatar element * @return {object} the newly created avatar element
*/ */
function createAvatar(jid) { function createAvatar(jid) {
var avatar = document.createElement('img'); let avatar = document.createElement('img');
avatar.className = "icon-avatar avatar"; avatar.className = "icon-avatar avatar";
avatar.src = Avatar.getAvatarUrl(jid); avatar.src = Avatar.getAvatarUrl(jid);
@ -43,12 +44,12 @@ function createAvatar(jid) {
* @param displayName the display name to set * @param displayName the display name to set
*/ */
function createDisplayNameParagraph(key, displayName) { function createDisplayNameParagraph(key, displayName) {
var p = document.createElement('p'); let p = document.createElement('p');
if(displayName) if (displayName) {
p.innerText = displayName; p.innerHTML = displayName;
else if(key) { } else if(key) {
p.setAttribute("data-i18n",key); p.setAttribute("data-i18n",key);
p.innerText = APP.translation.translateString(key); p.innerHTML = APP.translation.translateString(key);
} }
return p; return p;
@ -64,93 +65,79 @@ function stopGlowing(glower) {
} }
} }
function getContactEl (id) {
return $(`#contacts>li[id="${id}"]`);
}
function contactElExists (id) {
return getContactEl(id).length > 0;
}
/** /**
* Contact list. * Contact list.
*/ */
var ContactList = { var ContactList = {
init (emitter) {
this.emitter = emitter;
},
/** /**
* Indicates if the chat is currently visible. * Indicates if the chat is currently visible.
* *
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> - * @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
* otherwise * otherwise
*/ */
isVisible: function () { isVisible () {
return $('#contactlist').is(":visible"); return $('#contactlist').is(":visible");
}, },
/** /**
* Adds a contact for the given peerJid if such doesn't yet exist. * Adds a contact for the given id.
* *
* @param peerJid the peerJid corresponding to the contact
*/ */
ensureAddContact: function (peerJid) { addContact (id) {
var resourceJid = Strophe.getResourceFromJid(peerJid); let contactlist = $('#contacts');
var contact = $('#contacts>li[id="' + resourceJid + '"]'); let newContact = document.createElement('li');
newContact.id = id;
if (!contact || contact.length <= 0)
ContactList.addContact(peerJid);
},
/**
* Adds a contact for the given peer jid.
*
* @param peerJid the jid of the contact to add
*/
addContact: function (peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contactlist = $('#contacts');
var newContact = document.createElement('li');
newContact.id = resourceJid;
newContact.className = "clickable"; newContact.className = "clickable";
newContact.onclick = function (event) { newContact.onclick = (event) => {
if (event.currentTarget.className === "clickable") { if (event.currentTarget.className === "clickable") {
$(ContactList).trigger('contactclicked', [peerJid]); this.emitter.emit(UIEvents.CONTACT_CLICKED, id);
} }
}; };
newContact.appendChild(createAvatar(peerJid)); newContact.appendChild(createAvatar(id));
newContact.appendChild(createDisplayNameParagraph("participant")); newContact.appendChild(createDisplayNameParagraph("participant"));
if (resourceJid === APP.xmpp.myResource()) { if (APP.conference.isLocalId(id)) {
contactlist.prepend(newContact); contactlist.prepend(newContact);
} } else {
else {
contactlist.append(newContact); contactlist.append(newContact);
} }
updateNumberOfParticipants(1); updateNumberOfParticipants(1);
}, },
/** /**
* Removes a contact for the given peer jid. * Removes a contact for the given id.
* *
* @param peerJid the peerJid corresponding to the contact to remove
*/ */
removeContact: function (peerJid) { removeContact (id) {
var resourceJid = Strophe.getResourceFromJid(peerJid); let contact = getContactEl(id);
var contact = $('#contacts>li[id="' + resourceJid + '"]');
if (contact && contact.length > 0) {
var contactlist = $('#contactlist>ul');
contactlist.get(0).removeChild(contact.get(0));
if (contact.length > 0) {
contact.remove();
updateNumberOfParticipants(-1); updateNumberOfParticipants(-1);
} }
}, },
setVisualNotification: function (show, stopGlowingIn) { setVisualNotification (show, stopGlowingIn) {
var glower = $('#contactListButton'); let glower = $('#contactListButton');
if (show && !notificationInterval) { if (show && !notificationInterval) {
notificationInterval = window.setInterval(function () { notificationInterval = window.setInterval(function () {
glower.toggleClass('active glowing'); glower.toggleClass('active glowing');
}, 800); }, 800);
} } else if (!show && notificationInterval) {
else if (!show && notificationInterval) {
stopGlowing(glower); stopGlowing(glower);
} }
if (stopGlowingIn) { if (stopGlowingIn) {
@ -160,35 +147,28 @@ var ContactList = {
} }
}, },
setClickable: function (resourceJid, isClickable) { setClickable (id, isClickable) {
var contact = $('#contacts>li[id="' + resourceJid + '"]'); getContactEl(id).toggleClass('clickable', isClickable);
if (isClickable) {
contact.addClass('clickable');
} else {
contact.removeClass('clickable');
}
}, },
onDisplayNameChange: function (peerJid, displayName) { onDisplayNameChange (id, displayName) {
if (peerJid === 'localVideoContainer') if (id === 'localVideoContainer') {
peerJid = APP.xmpp.myJid(); id = APP.conference.localId;
}
let contactName = $(`#contacts #${id}>p`);
var resourceJid = Strophe.getResourceFromJid(peerJid); if (displayName) {
var contactName = $('#contacts #' + resourceJid + '>p');
if (contactName && displayName && displayName.length > 0)
contactName.html(displayName); contactName.html(displayName);
}
}, },
userAvatarChanged: function (resourceJid, avatarUrl) { changeUserAvatar (id, avatarUrl) {
// set the avatar in the contact list // set the avatar in the contact list
var contact = $('#' + resourceJid + '>img'); let contact = $(`#${id}>img`);
if (contact && contact.length > 0) { if (contact.length > 0) {
contact.get(0).src = avatarUrl; contact.attr('src', avatarUrl);
} }
} }
}; };
module.exports = ContactList; export default ContactList;

View File

@ -1,12 +1,12 @@
/* global APP, $ */ /* global APP, $ */
var Avatar = require("../../avatar/Avatar"); import UIUtil from "../../util/UIUtil";
var Settings = require("./../../../settings/Settings"); import UIEvents from "../../../../service/UI/UIEvents";
var UIUtil = require("../../util/UIUtil"); import languages from "../../../../service/translation/languages";
var languages = require("../../../../service/translation/languages"); import Settings from '../../../settings/Settings';
function generateLanguagesSelectBox() { function generateLanguagesSelectBox() {
var currentLang = APP.translation.getCurrentLanguage(); var currentLang = APP.translation.getCurrentLanguage();
var html = "<select id=\"languages_selectbox\">"; var html = '<select id="languages_selectbox">';
var langArray = languages.getLanguages(); var langArray = languages.getLanguages();
for(var i = 0; i < langArray.length; i++) { for(var i = 0; i < langArray.length; i++) {
var lang = langArray[i]; var lang = langArray[i];
@ -22,32 +22,54 @@ function generateLanguagesSelectBox() {
} }
var SettingsMenu = { export default {
init (emitter) {
function update() {
let displayName = UIUtil.escapeHtml($('#setDisplayName').val());
init: function () { if (displayName && Settings.getDisplayName() !== displayName) {
var startMutedSelector = $("#startMutedOptions"); emitter.emit(UIEvents.NICKNAME_CHANGED, displayName);
startMutedSelector.before(generateLanguagesSelectBox());
APP.translation.translateElement($("#languages_selectbox"));
$('#settingsmenu>input').keyup(function(event){
if(event.keyCode === 13) {//enter
SettingsMenu.update();
} }
});
if (APP.xmpp.isModerator()) { let language = $("#languages_selectbox").val();
startMutedSelector.css("display", "block"); if (language !== Settings.getLanguage()) {
} emitter.emit(UIEvents.LANG_CHANGED, language);
else { }
startMutedSelector.css("display", "none");
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 () { let startMutedBlock = $("#startMutedOptions");
SettingsMenu.update(); 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 () { onRoleChanged () {
if(APP.xmpp.isModerator()) { if(APP.conference.isModerator) {
$("#startMutedOptions").css("display", "block"); $("#startMutedOptions").css("display", "block");
} }
else { else {
@ -55,55 +77,22 @@ var SettingsMenu = {
} }
}, },
setStartMuted: function (audio, video) { onStartMutedChanged () {
$("#startAudioMuted").attr("checked", audio); $("#startAudioMuted").attr("checked", APP.conference.startAudioMuted);
$("#startVideoMuted").attr("checked", video); $("#startVideoMuted").attr("checked", APP.conference.startVideoMuted);
}, },
update: function() { isVisible () {
var newDisplayName =
UIUtil.escapeHtml($('#setDisplayName').get(0).value);
var newEmail = UIUtil.escapeHtml($('#setEmail').get(0).value);
if(newDisplayName) {
var displayName = Settings.setDisplayName(newDisplayName);
APP.xmpp.addToPresence("displayName", displayName, true);
}
var language = $("#languages_selectbox").val();
APP.translation.setLanguage(language);
Settings.setLanguage(language);
APP.xmpp.addToPresence("email", newEmail);
var email = Settings.setEmail(newEmail);
var startAudioMuted = ($("#startAudioMuted").is(":checked"));
var startVideoMuted = ($("#startVideoMuted").is(":checked"));
APP.xmpp.addToPresence("startMuted",
[startAudioMuted, startVideoMuted]);
Avatar.setUserAvatar(APP.xmpp.myJid(), email);
},
isVisible: function() {
return $('#settingsmenu').is(':visible'); return $('#settingsmenu').is(':visible');
}, },
setDisplayName: function(newDisplayName) { onDisplayNameChange (id, newDisplayName) {
var displayName = Settings.setDisplayName(newDisplayName); if(id === 'localVideoContainer' || APP.conference.isLocalId(id)) {
$('#setDisplayName').get(0).value = displayName; $('#setDisplayName').val(newDisplayName);
},
onDisplayNameChange: function(peerJid, newDisplayName) {
if(peerJid === 'localVideoContainer' ||
peerJid === APP.xmpp.myJid()) {
this.setDisplayName(newDisplayName);
} }
}, },
changeAvatar: function (thumbUrl) {
$('#avatar').get(0).src = thumbUrl; changeAvatar (avatarUrl) {
$('#avatar').attr('src', avatarUrl);
} }
}; };
module.exports = SettingsMenu;

View File

@ -1,66 +1,109 @@
/* global $ */ /* global $ */
var PanelToggler = require("../side_pannels/SidePanelToggler"); import UIUtil from '../util/UIUtil';
var UIUtil = require("../util/UIUtil"); import UIEvents from '../../../service/UI/UIEvents';
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter"); import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
var UIEvents = require("../../../service/UI/UIEvents");
var eventEmitter = null; const defaultBottomToolbarButtons = {
'chat': '#bottom_toolbar_chat',
var buttonHandlers = { 'contacts': '#bottom_toolbar_contact_list',
"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',
'filmstrip': '#bottom_toolbar_film_strip' 'filmstrip': '#bottom_toolbar_film_strip'
}; };
const BottomToolbar = {
init () {
this.filmStrip = $('#remoteVideos');
this.toolbar = $('#bottomToolbar');
},
var BottomToolbar = (function (my) { setupListeners (emitter) {
my.init = function (emitter) {
eventEmitter = emitter;
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons); UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
for(var k in buttonHandlers) const buttonHandlers = {
$("#" + k).click(buttonHandlers[k]); "bottom_toolbar_contact_list": function () {
}; AnalyticsAdapter.sendEvent('bottomtoolbar.contacts.toggled');
emitter.emit(UIEvents.TOGGLE_CONTACT_LIST);
},
"bottom_toolbar_film_strip": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.filmstrip.toggled');
emitter.emit(UIEvents.TOGGLE_FILM_STRIP);
},
"bottom_toolbar_chat": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.chat.toggled');
emitter.emit(UIEvents.TOGGLE_CHAT);
}
};
my.toggleChat = function() { Object.keys(buttonHandlers).forEach(
PanelToggler.toggleChat(); buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
}; );
},
my.toggleContactList = function() { toggleFilmStrip () {
PanelToggler.toggleContactList(); this.filmStrip.toggleClass("hidden");
}; },
my.toggleFilmStrip = function() { isFilmStripVisible () {
var filmstrip = $("#remoteVideos"); return !this.filmStrip.hasClass('hidden');
filmstrip.toggleClass("hidden"); },
eventEmitter.emit( UIEvents.FILM_STRIP_TOGGLED, setupFilmStripOnly () {
filmstrip.hasClass("hidden")); this.filmStrip.css({
}; padding: "0px 0px 18px 0px",
right: 0
});
},
$(document).bind("remotevideo.resized", function (event, width, height) { getFilmStripHeight () {
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18; if (this.isFilmStripVisible()) {
return this.filmStrip.outerHeight();
} else {
return 0;
}
},
$('#bottomToolbar').css({bottom: bottom + 'px'}); getFilmStripWidth () {
}); return this.filmStrip.width();
},
return my; resizeThumbnails (thumbWidth, thumbHeight,
}(BottomToolbar || {})); 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;

View File

@ -1,70 +1,134 @@
/* global APP, $, buttonClick, config, lockRoom, interfaceConfig, setSharedKey, /* global APP, $, config, interfaceConfig */
Util */
/* jshint -W101 */ /* jshint -W101 */
var messageHandler = require("../util/MessageHandler"); import messageHandler from '../util/MessageHandler';
var BottomToolbar = require("./BottomToolbar"); import UIUtil from '../util/UIUtil';
var Prezi = require("../prezi/Prezi"); import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
var Etherpad = require("../etherpad/Etherpad"); import UIEvents from '../../../service/UI/UIEvents';
var PanelToggler = require("../side_pannels/SidePanelToggler");
var Authentication = require("../authentication/Authentication");
var UIUtil = require("../util/UIUtil");
var AuthenticationEvents
= require("../../../service/authentication/AuthenticationEvents");
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter");
var Feedback = require("../Feedback");
var roomUrl = null; let roomUrl = null;
var sharedKey = ''; let recordingToaster = null;
var UI = null; let emitter = null;
var recordingToaster = null;
var buttonHandlers = {
/**
* Opens the invite link dialog.
*/
function openLinkDialog () {
let inviteAttributes;
if (roomUrl === null) {
inviteAttributes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
APP.translation.translateString("roomUrlDefaultMsg") + '"';
} else {
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
}
messageHandler.openTwoButtonDialog(
"dialog.shareLink", null, null,
`<input id="inviteLinkRef" type="text" ${inviteAttributes} onclick="this.select();" readonly>`,
false, "dialog.Invite",
function (e, v) {
if (v && roomUrl) {
emitter.emit(UIEvents.USER_INVITED, roomUrl);
}
},
function (event) {
if (roomUrl) {
document.getElementById('inviteLinkRef').select();
} else {
if (event && event.target) {
$(event.target).find('button[value=true]').prop('disabled', true);
}
}
}
);
}
// Sets the state of the recording button
function setRecordingButtonState (recordingState) {
let selector = $('#toolbar_button_record');
if (recordingState === 'on') {
selector.removeClass("icon-recEnable");
selector.addClass("icon-recEnable active");
$("#largeVideo").toggleClass("videoMessageFilter", true);
let recordOnKey = "recording.on";
$('#videoConnectionMessage').attr("data-i18n", recordOnKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
setTimeout(function(){
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
}, 1500);
recordingToaster = messageHandler.notify(
null, "recording.toaster", null,
null, null,
{timeOut: 0, closeButton: null, tapToDismiss: false}
);
} else if (recordingState === 'off') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
if (recordingToaster) {
messageHandler.remove(recordingToaster);
}
} else if (recordingState === 'pending') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", true);
let recordPendingKey = "recording.pending";
$('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
$('#videoConnectionMessage').css({display: "block"});
}
}
const buttonHandlers = {
"toolbar_button_mute": function () { "toolbar_button_mute": function () {
if (APP.RTC.localAudio.isMuted()) { if (APP.conference.audioMuted) {
AnalyticsAdapter.sendEvent('toolbar.audio.unmuted'); AnalyticsAdapter.sendEvent('toolbar.audio.unmuted');
emitter.emit(UIEvents.AUDIO_MUTED, false);
} else { } else {
AnalyticsAdapter.sendEvent('toolbar.audio.muted'); AnalyticsAdapter.sendEvent('toolbar.audio.muted');
emitter.emit(UIEvents.AUDIO_MUTED, true);
} }
return APP.UI.toggleAudio();
}, },
"toolbar_button_camera": function () { "toolbar_button_camera": function () {
if (APP.RTC.localVideo.isMuted()) { if (APP.conference.videoMuted) {
AnalyticsAdapter.sendEvent('toolbar.video.enabled'); AnalyticsAdapter.sendEvent('toolbar.video.enabled');
emitter.emit(UIEvents.VIDEO_MUTED, false);
} else { } else {
AnalyticsAdapter.sendEvent('toolbar.video.disabled'); AnalyticsAdapter.sendEvent('toolbar.video.disabled');
emitter.emit(UIEvents.VIDEO_MUTED, true);
} }
return APP.UI.toggleVideo();
}, },
/*"toolbar_button_authentication": function () {
return Toolbar.authenticateClicked();
},*/
"toolbar_button_record": function () { "toolbar_button_record": function () {
AnalyticsAdapter.sendEvent('toolbar.recording.toggled'); AnalyticsAdapter.sendEvent('toolbar.recording.toggled');
return toggleRecording(); emitter.emit(UIEvents.RECORDING_TOGGLE);
}, },
"toolbar_button_security": function () { "toolbar_button_security": function () {
if (sharedKey) { emitter.emit(UIEvents.ROOM_LOCK_CLICKED);
AnalyticsAdapter.sendEvent('toolbar.lock.disabled');
} else {
AnalyticsAdapter.sendEvent('toolbar.lock.enabled');
}
return Toolbar.openLockDialog();
}, },
"toolbar_button_link": function () { "toolbar_button_link": function () {
AnalyticsAdapter.sendEvent('toolbar.invite.clicked'); AnalyticsAdapter.sendEvent('toolbar.invite.clicked');
return Toolbar.openLinkDialog(); openLinkDialog();
}, },
"toolbar_button_chat": function () { "toolbar_button_chat": function () {
AnalyticsAdapter.sendEvent('toolbar.chat.toggled'); AnalyticsAdapter.sendEvent('toolbar.chat.toggled');
return BottomToolbar.toggleChat(); emitter.emit(UIEvents.TOGGLE_CHAT);
}, },
"toolbar_button_prezi": function () { "toolbar_button_prezi": function () {
AnalyticsAdapter.sendEvent('toolbar.prezi.clicked'); AnalyticsAdapter.sendEvent('toolbar.prezi.clicked');
return Prezi.openPreziDialog(); emitter.emit(UIEvents.PREZI_CLICKED);
}, },
"toolbar_button_etherpad": function () { "toolbar_button_etherpad": function () {
AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked'); AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked');
return Etherpad.toggleEtherpad(0); emitter.emit(UIEvents.ETHERPAD_CLICKED);
}, },
"toolbar_button_desktopsharing": function () { "toolbar_button_desktopsharing": function () {
if (APP.desktopsharing.isUsingScreenStream) { if (APP.desktopsharing.isUsingScreenStream) {
@ -72,32 +136,32 @@ var buttonHandlers = {
} else { } else {
AnalyticsAdapter.sendEvent('toolbar.screen.enabled'); AnalyticsAdapter.sendEvent('toolbar.screen.enabled');
} }
return APP.desktopsharing.toggleScreenSharing(); emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
}, },
"toolbar_button_fullScreen": function() { "toolbar_button_fullScreen": function() {
AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled'); AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled');
UIUtil.buttonClick("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen"); UIUtil.buttonClick("#toolbar_button_fullScreen", "icon-full-screen icon-exit-full-screen");
return Toolbar.toggleFullScreen(); emitter.emit(UIEvents.FULLSCREEN_TOGGLE);
}, },
"toolbar_button_sip": function () { "toolbar_button_sip": function () {
AnalyticsAdapter.sendEvent('toolbar.sip.clicked'); AnalyticsAdapter.sendEvent('toolbar.sip.clicked');
return callSipButtonClicked(); showSipNumberInput();
}, },
"toolbar_button_dialpad": function () { "toolbar_button_dialpad": function () {
AnalyticsAdapter.sendEvent('toolbar.sip.dialpad.clicked'); AnalyticsAdapter.sendEvent('toolbar.sip.dialpad.clicked');
return dialpadButtonClicked(); dialpadButtonClicked();
}, },
"toolbar_button_settings": function () { "toolbar_button_settings": function () {
AnalyticsAdapter.sendEvent('toolbar.settings.toggled'); AnalyticsAdapter.sendEvent('toolbar.settings.toggled');
PanelToggler.toggleSettingsMenu(); emitter.emit(UIEvents.TOGGLE_SETTINGS);
}, },
"toolbar_button_hangup": function () { "toolbar_button_hangup": function () {
AnalyticsAdapter.sendEvent('toolbar.hangup'); AnalyticsAdapter.sendEvent('toolbar.hangup');
return hangup(); emitter.emit(UIEvents.HANGUP);
}, },
"toolbar_button_login": function () { "toolbar_button_login": function () {
AnalyticsAdapter.sendEvent('toolbar.authenticate.login.clicked'); AnalyticsAdapter.sendEvent('toolbar.authenticate.login.clicked');
Toolbar.authenticateClicked(); emitter.emit(UIEvents.AUTH_CLICKED);
}, },
"toolbar_button_logout": function () { "toolbar_button_logout": function () {
AnalyticsAdapter.sendEvent('toolbar.authenticate.logout.clicked'); AnalyticsAdapter.sendEvent('toolbar.authenticate.logout.clicked');
@ -111,631 +175,218 @@ var buttonHandlers = {
"dialog.Yes", "dialog.Yes",
function (evt, yes) { function (evt, yes) {
if (yes) { if (yes) {
APP.xmpp.logout(function (url) { emitter.emit(UIEvents.LOGOUT);
if (url) {
window.location.href = url;
} else {
hangup();
}
});
} }
}); }
}
};
var defaultToolbarButtons = {
'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 " + const defaultToolbarButtons = {
APP.translation.translateString("email.and") + " Opera"; 'microphone': '#toolbar_button_mute',
var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1); 'camera': '#toolbar_button_camera',
var subject = APP.translation.translateString("email.subject", 'desktop': '#toolbar_button_desktopsharing',
{appName:interfaceConfig.APP_NAME, conferenceName: conferenceName}); 'security': '#toolbar_button_security',
var body = APP.translation.translateString("email.body", 'invite': '#toolbar_button_link',
{appName:interfaceConfig.APP_NAME, sharedKeyText: sharedKeyText, 'chat': '#toolbar_button_chat',
roomUrl: roomUrl, supportedBrowsers: supportedBrowsers}); 'prezi': '#toolbar_button_prezi',
body = body.replace(/\n/g, "%0D%0A"); 'etherpad': '#toolbar_button_etherpad',
'fullscreen': '#toolbar_button_fullScreen',
if (window.localStorage.displayname) { 'settings': '#toolbar_button_settings',
body += "%0D%0A%0D%0A" + window.localStorage.displayname; 'hangup': '#toolbar_button_hangup'
} };
if (interfaceConfig.INVITATION_POWERED_BY) {
body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
}
window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
}
function dialpadButtonClicked() { function dialpadButtonClicked() {
//TODO show the dialpad box //TODO show the dialpad box
} }
function callSipButtonClicked() { function showSipNumberInput () {
var defaultNumber let defaultNumber = config.defaultSipNumber
= config.defaultSipNumber ? config.defaultSipNumber : ''; ? config.defaultSipNumber
: '';
var sipMsg = APP.translation.generateTranslationHTML( let sipMsg = APP.translation.generateTranslationHTML("dialog.sipMsg");
"dialog.sipMsg"); messageHandler.openTwoButtonDialog(
messageHandler.openTwoButtonDialog(null, null, null, null, null, null,
'<h2>' + sipMsg + '</h2>' + `<h2>${sipMsg}</h2>
'<input name="sipNumber" type="text"' + <input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
' value="' + defaultNumber + '" autofocus>', false, "dialog.Dial",
false,
"dialog.Dial",
function (e, v, m, f) { function (e, v, m, f) {
if (v) { if (v && f.sipNumber) {
var numberInput = f.sipNumber; emitter.emit(UIEvents.SIP_DIAL, f.sipNumber);
if (numberInput) {
APP.xmpp.dial(
numberInput, 'fromnumber', UI.getRoomName(), sharedKey);
}
} }
}, },
null, null, ':input:first' null, null, ':input:first'
); );
} }
var Toolbar = (function (my) { const Toolbar = {
init (eventEmitter) {
emitter = eventEmitter;
my.init = function (ui) {
UIUtil.hideDisabledButtons(defaultToolbarButtons); UIUtil.hideDisabledButtons(defaultToolbarButtons);
for(var k in buttonHandlers) Object.keys(buttonHandlers).forEach(
$("#" + k).click(buttonHandlers[k]); buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
UI = ui;
// Update login info
APP.xmpp.addListener(
AuthenticationEvents.IDENTITY_UPDATED,
function (authenticationEnabled, userIdentity) {
var loggedIn = false;
if (userIdentity) {
loggedIn = true;
}
Toolbar.showAuthenticateButton(authenticationEnabled);
if (authenticationEnabled) {
Toolbar.setAuthenticatedIdentity(userIdentity);
Toolbar.showLoginButton(!loggedIn);
Toolbar.showLogoutButton(loggedIn);
}
}
); );
}; },
/**
* Sets shared key
* @param sKey the shared key
*/
my.setSharedKey = function (sKey) {
sharedKey = sKey;
};
my.authenticateClicked = function () {
Authentication.focusAuthenticationWindow();
if (!APP.xmpp.isExternalAuthEnabled()) {
Authentication.xmppAuthenticate();
return;
}
// Get authentication URL
if (!APP.xmpp.isMUCJoined()) {
APP.xmpp.getLoginUrl(UI.getRoomName(), function (url) {
// If conference has not been started yet - redirect to login page
window.location.href = url;
});
} else {
APP.xmpp.getPopupLoginUrl(UI.getRoomName(), function (url) {
// Otherwise - open popup with authentication URL
var authenticationWindow = Authentication.createAuthenticationWindow(
function () {
// On popup closed - retry room allocation
APP.xmpp.allocateConferenceFocus(
APP.UI.getRoomName(),
function () { console.info("AUTH DONE"); }
);
}, url);
if (!authenticationWindow) {
messageHandler.openMessageDialog(
null, "dialog.popupError");
}
});
}
};
/** /**
* Updates the room invite url. * Updates the room invite url.
*/ */
my.updateRoomUrl = function (newRoomUrl) { updateRoomUrl (newRoomUrl) {
roomUrl = newRoomUrl; roomUrl = newRoomUrl;
// If the invite dialog has been already opened we update the information. // If the invite dialog has been already opened we update the information.
var inviteLink = document.getElementById('inviteLinkRef'); let inviteLink = document.getElementById('inviteLinkRef');
if (inviteLink) { if (inviteLink) {
inviteLink.value = roomUrl; inviteLink.value = roomUrl;
inviteLink.select(); inviteLink.select();
$('#inviteLinkRef').parent() $('#inviteLinkRef').parent()
.find('button[value=true]').prop('disabled', false); .find('button[value=true]').prop('disabled', false);
} }
}; },
/** /**
* Disables and enables some of the buttons. * Disables and enables some of the buttons.
*/ */
my.setupButtonsFromConfig = function () { setupButtonsFromConfig () {
if (!UIUtil.isButtonEnabled('prezi')) { if (!UIUtil.isButtonEnabled('prezi')) {
$("#toolbar_button_prezi").css({display: "none"}); $("#toolbar_button_prezi").css({display: "none"});
} }
}; },
/**
* Opens the lock room dialog.
*/
my.openLockDialog = function () {
// Only the focus is able to set a shared key.
if (!APP.xmpp.isModerator()) {
if (sharedKey) {
messageHandler.openMessageDialog(null,
"dialog.passwordError");
} else {
messageHandler.openMessageDialog(null, "dialog.passwordError2");
}
} else {
if (sharedKey) {
messageHandler.openTwoButtonDialog(null, null,
"dialog.passwordCheck",
null,
false,
"dialog.Remove",
function (e, v) {
if (v) {
Toolbar.setSharedKey('');
lockRoom(false);
}
});
} else {
var msg = APP.translation.generateTranslationHTML(
"dialog.passwordMsg");
var yourPassword = APP.translation.translateString(
"dialog.yourPassword");
messageHandler.openTwoButtonDialog(null, null, null,
'<h2>' + msg + '</h2>' +
'<input name="lockKey" type="text"' +
' data-i18n="[placeholder]dialog.yourPassword" ' +
'placeholder="' + yourPassword + '" autofocus>',
false,
"dialog.Save",
function (e, v, m, f) {
if (v) {
var lockKey = f.lockKey;
if (lockKey) {
Toolbar.setSharedKey(
UIUtil.escapeHtml(lockKey));
lockRoom(true);
}
}
},
null, null, 'input:first'
);
}
}
};
/**
* Opens the invite link dialog.
*/
my.openLinkDialog = function () {
var inviteAttributes;
if (roomUrl === null) {
inviteAttributes = 'data-i18n="[value]roomUrlDefaultMsg" value="' +
APP.translation.translateString("roomUrlDefaultMsg") + '"';
} else {
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
}
messageHandler.openTwoButtonDialog("dialog.shareLink",
null, null,
'<input id="inviteLinkRef" type="text" ' +
inviteAttributes + ' onclick="this.select();" readonly>',
false,
"dialog.Invite",
function (e, v) {
if (v) {
if (roomUrl) {
inviteParticipants();
}
}
},
function (event) {
if (roomUrl) {
document.getElementById('inviteLinkRef').select();
} else {
if (event && event.target)
$(event.target)
.find('button[value=true]').prop('disabled', true);
}
}
);
};
/**
* Opens the settings dialog.
* FIXME: not used ?
*/
my.openSettingsDialog = function () {
var settings1 = APP.translation.generateTranslationHTML(
"dialog.settings1");
var settings2 = APP.translation.generateTranslationHTML(
"dialog.settings2");
var settings3 = APP.translation.generateTranslationHTML(
"dialog.settings3");
var yourPassword = APP.translation.translateString(
"dialog.yourPassword");
messageHandler.openTwoButtonDialog(null,
'<h2>' + settings1 + '</h2>' +
'<input type="checkbox" id="initMuted">' +
settings2 + '<br/>' +
'<input type="checkbox" id="requireNicknames">' +
settings3 +
'<input id="lockKey" type="text" placeholder="' + yourPassword +
'" data-i18n="[placeholder]dialog.yourPassword" autofocus>',
null,
null,
false,
"dialog.Save",
function () {
document.getElementById('lockKey').focus();
},
function (e, v) {
if (v) {
if ($('#initMuted').is(":checked")) {
// it is checked
}
if ($('#requireNicknames').is(":checked")) {
// it is checked
}
/*
var lockKey = document.getElementById('lockKey');
if (lockKey.value) {
setSharedKey(lockKey.value);
lockRoom(true);
}
*/
}
}
);
};
/**
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
my.toggleFullScreen = function () {
var fsElement = document.documentElement;
if (!document.mozFullScreen && !document.webkitIsFullScreen) {
//Enter Full Screen
if (fsElement.mozRequestFullScreen) {
fsElement.mozRequestFullScreen();
}
else {
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
//Exit Full Screen
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else {
document.webkitCancelFullScreen();
}
}
};
/** /**
* Unlocks the lock button state. * Unlocks the lock button state.
*/ */
my.unlockLockButton = function () { unlockLockButton () {
if ($("#toolbar_button_security").hasClass("icon-security-locked")) if ($("#toolbar_button_security").hasClass("icon-security-locked"))
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked"); UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
}; },
/** /**
* Updates the lock button state to locked. * Updates the lock button state to locked.
*/ */
my.lockLockButton = function () { lockLockButton () {
if ($("#toolbar_button_security").hasClass("icon-security")) if ($("#toolbar_button_security").hasClass("icon-security"))
UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked"); UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked");
}; },
/** /**
* Shows or hides authentication button * Shows or hides authentication button
* @param show <tt>true</tt> to show or <tt>false</tt> to hide * @param show <tt>true</tt> to show or <tt>false</tt> to hide
*/ */
my.showAuthenticateButton = function (show) { showAuthenticateButton (show) {
if (UIUtil.isButtonEnabled('authentication') && show) { if (UIUtil.isButtonEnabled('authentication') && show) {
$('#authentication').css({display: "inline"}); $('#authentication').css({display: "inline"});
} } else {
else {
$('#authentication').css({display: "none"}); $('#authentication').css({display: "none"});
} }
}; },
showEtherpadButton () {
if (!$('#toolbar_button_etherpad').is(":visible")) {
$('#toolbar_button_etherpad').css({display: 'inline-block'});
}
},
// Shows or hides the 'recording' button. // Shows or hides the 'recording' button.
my.showRecordingButton = function (show) { showRecordingButton (show) {
if (UIUtil.isButtonEnabled('recording') && show) { if (UIUtil.isButtonEnabled('recording') && show) {
$('#toolbar_button_record').css({display: "inline-block"}); $('#toolbar_button_record').css({display: "inline-block"});
} } else {
else {
$('#toolbar_button_record').css({display: "none"}); $('#toolbar_button_record').css({display: "none"});
} }
}; },
// Sets the state of the recording button
my.setRecordingButtonState = function (recordingState) {
var selector = $('#toolbar_button_record');
if (recordingState === 'on') {
selector.removeClass("icon-recEnable");
selector.addClass("icon-recEnable active");
$("#largeVideo").toggleClass("videoMessageFilter", true);
var recordOnKey = "recording.on";
$('#videoConnectionMessage').attr("data-i18n", recordOnKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey));
setTimeout(function(){
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
}, 1500);
recordingToaster = messageHandler.notify(null, "recording.toaster", null,
null, null, {timeOut: 0, closeButton: null, tapToDismiss: false});
} else if (recordingState === 'off') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", false);
$('#videoConnectionMessage').css({display: "none"});
if (recordingToaster)
messageHandler.remove(recordingToaster);
} else if (recordingState === 'pending') {
selector.removeClass("icon-recEnable active");
selector.addClass("icon-recEnable");
$("#largeVideo").toggleClass("videoMessageFilter", true);
var recordPendingKey = "recording.pending";
$('#videoConnectionMessage').attr("data-i18n", recordPendingKey);
$('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey));
$('#videoConnectionMessage').css({display: "block"});
}
};
// checks whether recording is enabled and whether we have params // checks whether recording is enabled and whether we have params
// to start automatically recording // to start automatically recording
my.checkAutoRecord = function () { checkAutoRecord () {
if (UIUtil.isButtonEnabled('recording') && config.autoRecord) { if (UIUtil.isButtonEnabled('recording') && config.autoRecord) {
toggleRecording(config.autoRecordToken); emitter.emit(UIEvents.RECORDING_TOGGLE, UIUtil.escapeHtml(config.autoRecordToken));
} }
}; },
// checks whether desktop sharing is enabled and whether // checks whether desktop sharing is enabled and whether
// we have params to start automatically sharing // we have params to start automatically sharing
my.checkAutoEnableDesktopSharing = function () { checkAutoEnableDesktopSharing () {
if (UIUtil.isButtonEnabled('desktop') if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) {
&& config.autoEnableDesktopSharing) { emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
APP.desktopsharing.toggleScreenSharing();
} }
}; },
// Shows or hides SIP calls button // Shows or hides SIP calls button
my.showSipCallButton = function (show) { showSipCallButton (show) {
if (APP.xmpp.isSipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) { if (APP.conference.sipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) {
$('#toolbar_button_sip').css({display: "inline-block"}); $('#toolbar_button_sip').css({display: "inline-block"});
} else { } else {
$('#toolbar_button_sip').css({display: "none"}); $('#toolbar_button_sip').css({display: "none"});
} }
}; },
// Shows or hides the dialpad button // Shows or hides the dialpad button
my.showDialPadButton = function (show) { showDialPadButton (show) {
if (UIUtil.isButtonEnabled('dialpad') && show) { if (UIUtil.isButtonEnabled('dialpad') && show) {
$('#toolbar_button_dialpad').css({display: "inline-block"}); $('#toolbar_button_dialpad').css({display: "inline-block"});
} else { } else {
$('#toolbar_button_dialpad').css({display: "none"}); $('#toolbar_button_dialpad').css({display: "none"});
} }
}; },
/** /**
* Displays user authenticated identity name(login). * Displays user authenticated identity name(login).
* @param authIdentity identity name to be displayed. * @param authIdentity identity name to be displayed.
*/ */
my.setAuthenticatedIdentity = function (authIdentity) { setAuthenticatedIdentity (authIdentity) {
if (authIdentity) { if (authIdentity) {
var selector = $('#toolbar_auth_identity'); let selector = $('#toolbar_auth_identity');
selector.css({display: "list-item"}); selector.css({display: "list-item"});
selector.text(authIdentity); selector.text(authIdentity);
} else { } else {
$('#toolbar_auth_identity').css({display: "none"}); $('#toolbar_auth_identity').css({display: "none"});
} }
}; },
/** /**
* Shows/hides login button. * Shows/hides login button.
* @param show <tt>true</tt> to show * @param show <tt>true</tt> to show
*/ */
my.showLoginButton = function (show) { showLoginButton (show) {
if (UIUtil.isButtonEnabled('authentication') && show) { if (UIUtil.isButtonEnabled('authentication') && show) {
$('#toolbar_button_login').css({display: "list-item"}); $('#toolbar_button_login').css({display: "list-item"});
} else { } else {
$('#toolbar_button_login').css({display: "none"}); $('#toolbar_button_login').css({display: "none"});
} }
}; },
/** /**
* Shows/hides logout button. * Shows/hides logout button.
* @param show <tt>true</tt> to show * @param show <tt>true</tt> to show
*/ */
my.showLogoutButton = function (show) { showLogoutButton (show) {
if (UIUtil.isButtonEnabled('authentication') && show) { if (UIUtil.isButtonEnabled('authentication') && show) {
$('#toolbar_button_logout').css({display: "list-item"}); $('#toolbar_button_logout').css({display: "list-item"});
} else { } else {
$('#toolbar_button_logout').css({display: "none"}); $('#toolbar_button_logout').css({display: "none"});
} }
}; },
/** /**
* Sets the state of the button. The button has blue glow if desktop * Sets the state of the button. The button has blue glow if desktop
* streaming is active. * streaming is active.
* @param active the state of the desktop streaming. * @param active the state of the desktop streaming.
*/ */
my.changeDesktopSharingButtonState = function (active) { changeDesktopSharingButtonState (active) {
var button = $("#toolbar_button_desktopsharing"); let button = $("#toolbar_button_desktopsharing");
if (active) { if (active) {
button.addClass("glow"); button.addClass("glow");
} else { } else {
button.removeClass("glow"); button.removeClass("glow");
} }
}; },
return my; updateRecordingState (state) {
}(Toolbar || {})); setRecordingButtonState(state);
}
};
module.exports = Toolbar; export default Toolbar;

View File

@ -1,9 +1,10 @@
/* global APP, config, $, interfaceConfig, Moderator, /* global APP, config, $, interfaceConfig */
DesktopStreaming.showDesktopSharingButton */
var toolbarTimeoutObject, import UIUtil from '../util/UIUtil';
toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT, import BottomToolbar from './BottomToolbar';
UIUtil = require("../util/UIUtil");
let toolbarTimeoutObject;
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
function showDesktopSharingButton() { function showDesktopSharingButton() {
if (APP.desktopsharing.isDesktopSharingEnabled() && if (APP.desktopsharing.isDesktopSharingEnabled() &&
@ -14,19 +15,24 @@ function showDesktopSharingButton() {
} }
} }
function isToolbarVisible () {
return $('#header').is(':visible');
}
/** /**
* Hides the toolbar. * Hides the toolbar.
*/ */
function hideToolbar() { function hideToolbar() {
if(config.alwaysVisibleToolbar) if (config.alwaysVisibleToolbar) {
return; return;
}
var header = $("#header"), let header = $("#header");
bottomToolbar = $("#bottomToolbar"); let bottomToolbar = $("#bottomToolbar");
var isToolbarHover = false; let isToolbarHover = false;
header.find('*').each(function () { header.find('*').each(function () {
var id = $(this).attr('id'); let id = $(this).attr('id');
if ($("#" + id + ":hover").length > 0) { if ($(`#${id}:hover`).length > 0) {
isToolbarHover = true; isToolbarHover = true;
} }
}); });
@ -37,34 +43,36 @@ function hideToolbar() {
clearTimeout(toolbarTimeoutObject); clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null; toolbarTimeoutObject = null;
if (!isToolbarHover) { if (isToolbarHover) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else {
header.hide("slide", { direction: "up", duration: 300}); header.hide("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "-=40"}, 300); $('#subject').animate({top: "-=40"}, 300);
if ($("#remoteVideos").hasClass("hidden")) { if (!BottomToolbar.isFilmStripVisible()) {
bottomToolbar.hide( bottomToolbar.hide(
"slide", {direction: "right", duration: 300}); "slide", {direction: "right", duration: 300}
);
} }
} }
else {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
}
} }
var ToolbarToggler = { const ToolbarToggler = {
/** /**
* Shows the main toolbar. * Shows the main toolbar.
*/ */
showToolbar: function () { showToolbar () {
if (interfaceConfig.filmStripOnly) if (interfaceConfig.filmStripOnly) {
return; return;
var header = $("#header"), }
bottomToolbar = $("#bottomToolbar"); let header = $("#header");
let bottomToolbar = $("#bottomToolbar");
if (!header.is(':visible') || !bottomToolbar.is(":visible")) { if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
header.show("slide", { direction: "up", duration: 300}); header.show("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "+=40"}, 300); $('#subject').animate({top: "+=40"}, 300);
if (!bottomToolbar.is(":visible")) { if (!bottomToolbar.is(":visible")) {
bottomToolbar.show( bottomToolbar.show(
"slide", {direction: "right", duration: 300}); "slide", {direction: "right", duration: 300}
);
} }
if (toolbarTimeoutObject) { if (toolbarTimeoutObject) {
@ -75,13 +83,6 @@ var ToolbarToggler = {
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT; toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
} }
if (APP.xmpp.isModerator())
{
// TODO: Enable settings functionality.
// Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"});
}
// Show/hide desktop sharing button // Show/hide desktop sharing button
showDesktopSharingButton(); showDesktopSharingButton();
}, },
@ -91,33 +92,28 @@ var ToolbarToggler = {
* *
* @param isDock indicates what operation to perform * @param isDock indicates what operation to perform
*/ */
dockToolbar: function (isDock) { dockToolbar (isDock) {
if (interfaceConfig.filmStripOnly) if (interfaceConfig.filmStripOnly) {
return; return;
}
if (isDock) { if (isDock) {
// First make sure the toolbar is shown. // First make sure the toolbar is shown.
if (!$('#header').is(':visible')) { if (!isToolbarVisible()) {
this.showToolbar(); this.showToolbar();
} }
// Then clear the time out, to dock the toolbar. // Then clear the time out, to dock the toolbar.
if (toolbarTimeoutObject) { clearTimeout(toolbarTimeoutObject);
clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null;
toolbarTimeoutObject = null; } else {
} if (isToolbarVisible()) {
} toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
else { } else {
if (!$('#header').is(':visible')) {
this.showToolbar(); this.showToolbar();
} }
else {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
}
} }
}, }
showDesktopSharingButton: showDesktopSharingButton
}; };
module.exports = ToolbarToggler; module.exports = ToolbarToggler;

View File

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

View File

@ -1,17 +1,34 @@
/* global $, config, interfaceConfig */ /* global $, config, interfaceConfig */
/** /**
* Created by hristo on 12/22/14. * Created by hristo on 12/22/14.
*/ */
var UIUtil = module.exports = { var UIUtil = {
/**
* Returns the size of the side panel.
*/
getSidePanelSize () {
var availableHeight = window.innerHeight;
var availableWidth = window.innerWidth;
var panelWidth = 200;
if (availableWidth * 0.2 < 200) {
panelWidth = availableWidth * 0.2;
}
return [panelWidth, availableHeight];
},
/** /**
* Returns the available video width. * Returns the available video width.
*/ */
getAvailableVideoWidth: function (isVisible) { getAvailableVideoWidth: function (isSidePanelVisible) {
var PanelToggler = require("../side_pannels/SidePanelToggler"); let rightPanelWidth = 0;
if(typeof isVisible === "undefined" || isVisible === null)
isVisible = PanelToggler.isVisible(); if (isSidePanelVisible) {
var rightPanelWidth rightPanelWidth = UIUtil.getSidePanelSize()[0];
= isVisible ? PanelToggler.getPanelSize()[0] : 0; }
return window.innerWidth - rightPanelWidth; return window.innerWidth - rightPanelWidth;
}, },
@ -112,5 +129,17 @@ var UIUtil = module.exports = {
.filter(function (item) { return item; }) .filter(function (item) { return item; })
.join(','); .join(',');
$(selector).hide(); $(selector).hide();
} },
};
redirect (url) {
window.location.href = url;
},
isFullScreen () {
return document.fullScreen
|| document.mozFullScreen
|| document.webkitIsFullScreen;
}
};
export default UIUtil;

View File

@ -1,13 +1,13 @@
/* global APP, $ */ /* global APP, $ */
/* jshint -W101 */ /* jshint -W101 */
var JitsiPopover = require("../util/JitsiPopover"); import JitsiPopover from "../util/JitsiPopover";
/** /**
* Constructs new connection indicator. * Constructs new connection indicator.
* @param videoContainer the video container associated with the indicator. * @param videoContainer the video container associated with the indicator.
* @constructor * @constructor
*/ */
function ConnectionIndicator(videoContainer, jid) { function ConnectionIndicator(videoContainer, id) {
this.videoContainer = videoContainer; this.videoContainer = videoContainer;
this.bandwidth = null; this.bandwidth = null;
this.packetLoss = null; this.packetLoss = null;
@ -16,7 +16,7 @@ function ConnectionIndicator(videoContainer, jid) {
this.resolution = null; this.resolution = null;
this.transport = []; this.transport = [];
this.popover = null; this.popover = null;
this.jid = jid; this.id = id;
this.create(); this.create();
} }
@ -87,7 +87,7 @@ ConnectionIndicator.prototype.generateText = function () {
} }
var resolutionValue = null; var resolutionValue = null;
if(this.resolution && this.jid) { if(this.resolution && this.id) {
var keys = Object.keys(this.resolution); var keys = Object.keys(this.resolution);
for(var ssrc in this.resolution) { for(var ssrc in this.resolution) {
// skip resolutions for ssrc that don't have this info // skip resolutions for ssrc that don't have this info
@ -99,7 +99,7 @@ ConnectionIndicator.prototype.generateText = function () {
} }
} }
if(this.jid === null) { if(this.id === null) {
resolution = ""; resolution = "";
if(this.resolution === null || !Object.keys(this.resolution) || if(this.resolution === null || !Object.keys(this.resolution) ||
Object.keys(this.resolution).length === 0) { Object.keys(this.resolution).length === 0) {
@ -144,8 +144,8 @@ ConnectionIndicator.prototype.generateText = function () {
if(this.videoContainer.videoSpanId == "localVideoContainer") { if(this.videoContainer.videoSpanId == "localVideoContainer") {
result += "<div class=\"jitsipopover_showmore\" " + result += "<div class=\"jitsipopover_showmore\" " +
"onclick = \"APP.UI.connectionIndicatorShowMore('" + "onclick = \"APP.UI.connectionIndicatorShowMore('" +
// FIXME: we do not know local jid when this text is generated // FIXME: we do not know local id when this text is generated
//this.jid + "')\" data-i18n='connectionindicator." + //this.id + "')\" data-i18n='connectionindicator." +
"local')\" data-i18n='connectionindicator." + "local')\" data-i18n='connectionindicator." +
(this.showMoreValue ? "less" : "more") + "'>" + (this.showMoreValue ? "less" : "more") + "'>" +
translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) + translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) +
@ -365,7 +365,8 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) {
*/ */
ConnectionIndicator.prototype.updatePopoverData = function () { ConnectionIndicator.prototype.updatePopoverData = function () {
this.popover.updateContent( this.popover.updateContent(
"<div class=\"connection_info\">" + this.generateText() + "</div>"); `<div class="connection_info">${this.generateText()}</div>`
);
APP.translation.translateElement($(".connection_info")); APP.translation.translateElement($(".connection_info"));
}; };
@ -385,4 +386,4 @@ ConnectionIndicator.prototype.hideIndicator = function () {
this.popover.forceHide(); this.popover.forceHide();
}; };
module.exports = ConnectionIndicator; export default ConnectionIndicator;

View File

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

View File

@ -1,19 +1,23 @@
/* global $, interfaceConfig, APP */ /* global $, interfaceConfig, APP */
var SmallVideo = require("./SmallVideo"); import ConnectionIndicator from "./ConnectionIndicator";
var ConnectionIndicator = require("./ConnectionIndicator"); import UIUtil from "../util/UIUtil";
var NicknameHandler = require("../util/NicknameHandler"); import UIEvents from "../../../service/UI/UIEvents";
var UIUtil = require("../util/UIUtil"); import SmallVideo from "./SmallVideo";
var LargeVideo = require("./LargeVideo"); var LargeVideo = require("./LargeVideo");
var RTCBrowserType = require("../../RTC/RTCBrowserType"); var RTCBrowserType = require("../../RTC/RTCBrowserType");
function LocalVideo(VideoLayout) { const TrackEvents = JitsiMeetJS.events.track;
function LocalVideo(VideoLayout, emitter) {
this.videoSpanId = "localVideoContainer"; this.videoSpanId = "localVideoContainer";
this.container = $("#localVideoContainer").get(0); this.container = $("#localVideoContainer").get(0);
this.bindHoverHandler(); this.bindHoverHandler();
this.VideoLayout = VideoLayout; this.VideoLayout = VideoLayout;
this.flipX = true; this.flipX = true;
this.isLocal = true; this.isLocal = true;
this.peerJid = null; this.emitter = emitter;
SmallVideo.call(this);
} }
LocalVideo.prototype = Object.create(SmallVideo.prototype); LocalVideo.prototype = Object.create(SmallVideo.prototype);
@ -61,6 +65,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
$('#localDisplayName').html(defaultLocalDisplayName); $('#localDisplayName').html(defaultLocalDisplayName);
} }
} }
this.updateView();
} else { } else {
var editButton = createEditDisplayNameButton(); var editButton = createEditDisplayNameButton();
@ -117,12 +122,14 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
editDisplayName.one("focusout", function (e) { editDisplayName.one("focusout", function (e) {
self.VideoLayout.inputDisplayNameHandler(this.value); self.VideoLayout.inputDisplayNameHandler(this.value);
$('#editDisplayName').hide();
}); });
editDisplayName.on('keydown', function (e) { editDisplayName.on('keydown', function (e) {
if (e.keyCode === 13) { if (e.keyCode === 13) {
e.preventDefault(); e.preventDefault();
self.VideoLayout.inputDisplayNameHandler(this.value); $('#editDisplayName').hide();
// focusout handler will save display name
} }
}); });
}); });
@ -130,25 +137,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
}; };
LocalVideo.prototype.inputDisplayNameHandler = function (name) { LocalVideo.prototype.inputDisplayNameHandler = function (name) {
name = UIUtil.escapeHtml(name); this.emitter.emit(UIEvents.NICKNAME_CHANGED, UIUtil.escapeHtml(name));
NicknameHandler.setNickname(name);
var localDisplayName = $('#localDisplayName');
if (!localDisplayName.is(":visible")) {
if (NicknameHandler.getNickname()) {
var meHTML = APP.translation.generateTranslationHTML("me");
localDisplayName.html(NicknameHandler.getNickname() + " (" +
meHTML + ")");
} else {
var defaultHTML = APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
localDisplayName .html(defaultHTML);
}
localDisplayName.show();
}
$('#editDisplayName').hide();
}; };
LocalVideo.prototype.createConnectionIndicator = function() { LocalVideo.prototype.createConnectionIndicator = function() {
@ -158,37 +147,29 @@ LocalVideo.prototype.createConnectionIndicator = function() {
this.connectionIndicator = new ConnectionIndicator(this, null); this.connectionIndicator = new ConnectionIndicator(this, null);
}; };
LocalVideo.prototype.changeVideo = function (stream, isMuted) { LocalVideo.prototype.changeVideo = function (stream) {
var self = this; this.stream = stream;
function localVideoClick(event) { let localVideoClick = (event) => {
// FIXME: with Temasys plugin event arg is not an event, but // FIXME: with Temasys plugin event arg is not an event, but
// the clicked object itself, so we have to skip this call // the clicked object itself, so we have to skip this call
if (event.stopPropagation) { if (event.stopPropagation) {
event.stopPropagation(); event.stopPropagation();
} }
self.VideoLayout.handleVideoThumbClicked( this.VideoLayout.handleVideoThumbClicked(true, this.id);
true, };
APP.xmpp.myResource());
}
var localVideoContainerSelector = $('#localVideoContainer'); let localVideoContainerSelector = $('#localVideoContainer');
localVideoContainerSelector.off('click'); localVideoContainerSelector.off('click');
localVideoContainerSelector.on('click', localVideoClick); localVideoContainerSelector.on('click', localVideoClick);
if(isMuted) { this.flipX = stream.videoType != "desktop";
APP.UI.setVideoMute(true); let localVideo = document.createElement('video');
return; localVideo.id = 'localVideo_' + stream.getId();
}
this.flipX = stream.videoType != "screen";
var localVideo = document.createElement('video');
localVideo.id = 'localVideo_' +
APP.RTC.getStreamID(stream.getOriginalStream());
if (!RTCBrowserType.isIExplorer()) { if (!RTCBrowserType.isIExplorer()) {
localVideo.autoplay = true; localVideo.autoplay = true;
localVideo.volume = 0; // is it required if audio is separated ? localVideo.volume = 0; // is it required if audio is separated ?
} }
localVideo.oncontextmenu = function () { return false; };
var localVideoContainer = document.getElementById('localVideoWrapper'); var localVideoContainer = document.getElementById('localVideoWrapper');
// Put the new video always in front // Put the new video always in front
@ -207,29 +188,19 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
} }
// Attach WebRTC stream // Attach WebRTC stream
APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream()); stream.attach(localVideoSelector);
// Add stream ended handler let endedHandler = () => {
APP.RTC.addMediaStreamInactiveHandler(
stream.getOriginalStream(), function () {
// We have to re-select after attach when Temasys plugin is used,
// because <video> element is replaced with <object>
localVideo = $('#' + localVideo.id)[0]; localVideo = $('#' + localVideo.id)[0];
localVideoContainer.removeChild(localVideo); localVideoContainer.removeChild(localVideo);
self.VideoLayout.updateRemovedVideo(APP.xmpp.myResource()); this.VideoLayout.updateRemovedVideo(this.id);
}); stream.off(TrackEvents.TRACK_STOPPED, endedHandler);
};
stream.on(TrackEvents.TRACK_STOPPED, endedHandler);
}; };
LocalVideo.prototype.joined = function (jid) { LocalVideo.prototype.joined = function (id) {
this.peerJid = jid; this.id = id;
}; };
LocalVideo.prototype.getResourceJid = function () { export default LocalVideo;
var myResource = APP.xmpp.myResource();
if (!myResource) {
console.error("Requested local resource before we're in the MUC");
}
return myResource;
};
module.exports = LocalVideo;

View File

@ -1,28 +1,30 @@
/* global $, APP, require, Strophe, interfaceConfig */ /* global $, APP, interfaceConfig */
var ConnectionIndicator = require("./ConnectionIndicator");
var SmallVideo = require("./SmallVideo");
var AudioLevels = require("../audio_levels/AudioLevels");
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
var RTCBrowserType = require("../../RTC/RTCBrowserType");
var UIUtils = require("../util/UIUtil");
var XMPPEvents = require("../../../service/xmpp/XMPPEvents");
function RemoteVideo(peerJid, VideoLayout) { import ConnectionIndicator from './ConnectionIndicator';
this.peerJid = peerJid;
this.resourceJid = Strophe.getResourceFromJid(peerJid); import SmallVideo from "./SmallVideo";
this.videoSpanId = 'participant_' + this.resourceJid; import AudioLevels from "../audio_levels/AudioLevels";
import UIUtils from "../util/UIUtil";
import UIEvents from '../../../service/UI/UIEvents';
var RTCBrowserType = require("../../RTC/RTCBrowserType");
function RemoteVideo(id, VideoLayout, emitter) {
this.id = id;
this.emitter = emitter;
this.videoSpanId = `participant_${id}`;
this.VideoLayout = VideoLayout; this.VideoLayout = VideoLayout;
this.addRemoteVideoContainer(); this.addRemoteVideoContainer();
this.connectionIndicator = new ConnectionIndicator( this.connectionIndicator = new ConnectionIndicator(this, id);
this, this.peerJid);
this.setDisplayName(); this.setDisplayName();
var nickfield = document.createElement('span'); var nickfield = document.createElement('span');
nickfield.className = "nick"; nickfield.className = "nick";
nickfield.appendChild(document.createTextNode(this.resourceJid)); nickfield.appendChild(document.createTextNode(id));
this.container.appendChild(nickfield); this.container.appendChild(nickfield);
this.bindHoverHandler(); this.bindHoverHandler();
this.flipX = false; this.flipX = false;
this.isLocal = false; this.isLocal = false;
SmallVideo.call(this);
} }
RemoteVideo.prototype = Object.create(SmallVideo.prototype); RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@ -30,18 +32,20 @@ RemoteVideo.prototype.constructor = RemoteVideo;
RemoteVideo.prototype.addRemoteVideoContainer = function() { RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.container = RemoteVideo.createContainer(this.videoSpanId); this.container = RemoteVideo.createContainer(this.videoSpanId);
if (APP.xmpp.isModerator()) if (APP.conference.isModerator) {
this.addRemoteVideoMenu(); this.addRemoteVideoMenu();
AudioLevels.updateAudioLevelCanvas(this.peerJid, this.VideoLayout); }
let {thumbWidth, thumbHeight} = this.VideoLayout.calculateThumbnailSize();
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
return this.container; return this.container;
}; };
/** /**
* Adds the remote video menu element for the given <tt>jid</tt> in the * Adds the remote video menu element for the given <tt>id</tt> in the
* given <tt>parentElement</tt>. * given <tt>parentElement</tt>.
* *
* @param jid the jid indicating the video for which we're adding a menu. * @param id the id indicating the video for which we're adding a menu.
* @param parentElement the parent element where this menu will be added * @param parentElement the parent element where this menu will be added
*/ */
@ -60,7 +64,7 @@ if (!interfaceConfig.filmStripOnly) {
var popupmenuElement = document.createElement('ul'); var popupmenuElement = document.createElement('ul');
popupmenuElement.className = 'popupmenu'; popupmenuElement.className = 'popupmenu';
popupmenuElement.id = 'remote_popupmenu_' + this.getResourceJid(); popupmenuElement.id = `remote_popupmenu_${this.id}`;
spanElement.appendChild(popupmenuElement); spanElement.appendChild(popupmenuElement);
var muteMenuItem = document.createElement('li'); var muteMenuItem = document.createElement('li');
@ -88,7 +92,7 @@ if (!interfaceConfig.filmStripOnly) {
event.preventDefault(); event.preventDefault();
} }
var isMute = !!self.isMuted; var isMute = !!self.isMuted;
APP.xmpp.setMute(self.peerJid, !isMute); self.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, self.id);
popupmenuElement.setAttribute('style', 'display:none;'); popupmenuElement.setAttribute('style', 'display:none;');
@ -117,7 +121,7 @@ if (!interfaceConfig.filmStripOnly) {
"data-i18n='videothumbnail.kick'>&nbsp;</div>"; "data-i18n='videothumbnail.kick'>&nbsp;</div>";
ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText; ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText;
ejectLinkItem.onclick = function(){ ejectLinkItem.onclick = function(){
APP.xmpp.eject(self.peerJid); self.emitter.emit(UIEvents.USER_KICKED, self.id);
popupmenuElement.setAttribute('style', 'display:none;'); popupmenuElement.setAttribute('style', 'display:none;');
}; };
@ -157,48 +161,49 @@ RemoteVideo.prototype.removeRemoteStreamElement =
select.remove(); select.remove();
console.info((isVideo ? "Video" : "Audio") + console.info((isVideo ? "Video" : "Audio") +
" removed " + this.getResourceJid(), select); " removed " + this.id, select);
if (isVideo) if (isVideo)
this.VideoLayout.updateRemovedVideo(this.getResourceJid()); this.VideoLayout.updateRemovedVideo(this.id);
}; };
/** /**
* Removes RemoteVideo from the page. * Removes RemoteVideo from the page.
*/ */
RemoteVideo.prototype.remove = function () { RemoteVideo.prototype.remove = function () {
console.log("Remove thumbnail", this.peerJid); console.log("Remove thumbnail", this.id);
this.removeConnectionIndicator(); this.removeConnectionIndicator();
// Make sure that the large video is updated if are removing its // Make sure that the large video is updated if are removing its
// corresponding small video. // corresponding small video.
this.VideoLayout.updateRemovedVideo(this.getResourceJid()); this.VideoLayout.updateRemovedVideo(this.id);
// Remove whole container // Remove whole container
if (this.container.parentNode) if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container); this.container.parentNode.removeChild(this.container);
}
}; };
RemoteVideo.prototype.waitForPlayback = function (sel, stream) { RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
var webRtcStream = stream.getOriginalStream(); var webRtcStream = stream.getOriginalStream();
var isVideo = stream.isVideoStream(); var isVideo = stream.isVideoTrack();
if (!isVideo || webRtcStream.id === 'mixedmslabel') { if (!isVideo || webRtcStream.id === 'mixedmslabel') {
return; return;
} }
var self = this; var self = this;
var resourceJid = this.getResourceJid();
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
// when video playback starts // when video playback starts
var onPlayingHandler = function () { var onPlayingHandler = function () {
// FIXME: why do i have to do this for FF? // FIXME: why do i have to do this for FF?
if (RTCBrowserType.isFirefox()) { if (RTCBrowserType.isFirefox()) {
APP.RTC.attachMediaStream(sel, webRtcStream); //FIXME: weshould use the lib here
//APP.RTC.attachMediaStream(sel, webRtcStream);
} }
if (RTCBrowserType.isTemasysPluginUsed()) { if (RTCBrowserType.isTemasysPluginUsed()) {
sel = self.selectVideoElement(); sel = self.selectVideoElement();
} }
self.VideoLayout.videoactive(sel, resourceJid); self.VideoLayout.videoactive(sel, self.id);
sel[0].onplaying = null; sel[0].onplaying = null;
if (RTCBrowserType.isTemasysPluginUsed()) { if (RTCBrowserType.isTemasysPluginUsed()) {
// 'currentTime' is used to check if the video has started // 'currentTime' is used to check if the video has started
@ -210,39 +215,18 @@ RemoteVideo.prototype.waitForPlayback = function (sel, stream) {
}; };
RemoteVideo.prototype.addRemoteStreamElement = function (stream) { RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
if (!this.container) if (!this.container) {
return; return;
var self = this;
var webRtcStream = stream.getOriginalStream();
var isVideo = stream.isVideoStream();
var streamElement = SmallVideo.createStreamElement(stream);
var newElementId = streamElement.id;
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
var sel = $('#' + newElementId);
sel.hide();
// If the container is currently visible we attach the stream.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
this.waitForPlayback(sel, stream);
APP.RTC.attachMediaStream(sel, webRtcStream);
} }
APP.RTC.addMediaStreamInactiveHandler( this.stream = stream;
webRtcStream, function () {
console.log('stream ended', this);
self.removeRemoteStreamElement(webRtcStream, isVideo, newElementId); let isVideo = stream.isVideoTrack();
});
// Add click handler. // Add click handler.
var onClickHandler = function (event) { let onClickHandler = (event) => {
self.VideoLayout.handleVideoThumbClicked(false, self.getResourceJid()); this.VideoLayout.handleVideoThumbClicked(false, this.id);
// On IE we need to populate this handler on video <object> // On IE we need to populate this handler on video <object>
// and it does not give event instance as an argument, // and it does not give event instance as an argument,
@ -254,14 +238,39 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
return false; return false;
}; };
this.container.onclick = onClickHandler; this.container.onclick = onClickHandler;
if(!stream.getOriginalStream())
return;
let streamElement = SmallVideo.createStreamElement(stream);
let newElementId = streamElement.id;
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
let sel = $(`#${newElementId}`);
// If the container is currently visible we attach the stream.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
this.waitForPlayback(sel, stream);
stream.attach(sel);
}
// hide element only after stream was (maybe) attached
// because Temasys plugin requires video element
// to be visible to attach the stream
sel.hide();
// reselect // reselect
if (RTCBrowserType.isTemasysPluginUsed()) if (RTCBrowserType.isTemasysPluginUsed()) {
sel = $('#' + newElementId); sel = $(`#${newElementId}`);
sel[0].onclick = onClickHandler; }
sel.click(onClickHandler);
}, },
/** /**
* Show/hide peer container for the given resourceJid. * Show/hide peer container for the given id.
*/ */
RemoteVideo.prototype.showPeerContainer = function (state) { RemoteVideo.prototype.showPeerContainer = function (state) {
if (!this.container) if (!this.container)
@ -275,10 +284,10 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
resizeThumbnails = true; resizeThumbnails = true;
$(this.container).show(); $(this.container).show();
} }
// Call showAvatar with undefined, so that we'll figure out if avatar // Call updateView, so that we'll figure out if avatar
// should be displayed based on video muted status and whether or not // should be displayed based on video muted status and whether or not
// it's in the lastN set // it's in the lastN set
this.showAvatar(undefined); this.updateView();
} }
else if ($(this.container).is(':visible') && isHide) else if ($(this.container).is(':visible') && isHide)
{ {
@ -294,10 +303,16 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
// We want to be able to pin a participant from the contact list, even // We want to be able to pin a participant from the contact list, even
// if he's not in the lastN set! // if he's not in the lastN set!
// ContactList.setClickable(resourceJid, !isHide); // ContactList.setClickable(id, !isHide);
}; };
RemoteVideo.prototype.updateResolution = function (resolution) {
if (this.connectionIndicator) {
this.connectionIndicator.updateResolution(resolution);
}
};
RemoteVideo.prototype.removeConnectionIndicator = function () { RemoteVideo.prototype.removeConnectionIndicator = function () {
if (this.connectionIndicator) if (this.connectionIndicator)
this.connectionIndicator.remove(); this.connectionIndicator.remove();
@ -311,12 +326,11 @@ RemoteVideo.prototype.hideConnectionIndicator = function () {
/** /**
* Updates the remote video menu. * Updates the remote video menu.
* *
* @param jid the jid indicating the video for which we're adding a menu. * @param id the id indicating the video for which we're adding a menu.
* @param isMuted indicates the current mute state * @param isMuted indicates the current mute state
*/ */
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) { RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) {
var muteMenuItem var muteMenuItem = $(`#remote_popupmenu_${this.id}>li>a.mutelink`);
= $('#remote_popupmenu_' + this.getResourceJid() + '>li>a.mutelink');
var mutedIndicator = "<i class='icon-mic-disabled'></i>"; var mutedIndicator = "<i class='icon-mic-disabled'></i>";
@ -399,13 +413,11 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
nameSpan.className = 'displayname'; nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan); $('#' + this.videoSpanId)[0].appendChild(nameSpan);
if (displayName && displayName.length > 0) { if (displayName && displayName.length > 0) {
nameSpan.innerText = displayName; nameSpan.innerHTML = displayName;
} }
else else
nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; nameSpan.innerHTML = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
nameSpan.id = this.videoSpanId + '_name'; nameSpan.id = this.videoSpanId + '_name';
} }
}; };
@ -423,13 +435,6 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
} }
}; };
RemoteVideo.prototype.getResourceJid = function () {
if (!this.resourceJid) {
console.error("Undefined resource jid");
}
return this.resourceJid;
};
RemoteVideo.createContainer = function (spanId) { RemoteVideo.createContainer = function (spanId) {
var container = document.createElement('span'); var container = document.createElement('span');
container.id = spanId; container.id = spanId;
@ -439,4 +444,4 @@ RemoteVideo.createContainer = function (spanId) {
}; };
module.exports = RemoteVideo; export default RemoteVideo;

View File

@ -1,14 +1,15 @@
/* global $, APP, require */ /* global $, APP, require */
/* jshint -W101 */ /* jshint -W101 */
var Avatar = require("../avatar/Avatar"); import Avatar from "../avatar/Avatar";
var UIUtil = require("../util/UIUtil"); import UIUtil from "../util/UIUtil";
var LargeVideo = require("./LargeVideo");
var RTCBrowserType = require("../../RTC/RTCBrowserType"); var RTCBrowserType = require("../../RTC/RTCBrowserType");
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
function SmallVideo() { function SmallVideo() {
this.isMuted = false; this.isMuted = false;
this.hasAvatar = false; this.hasAvatar = false;
this.isVideoMuted = false;
this.stream = null;
} }
function setVisibility(selector, show) { function setVisibility(selector, show) {
@ -17,6 +18,16 @@ function setVisibility(selector, show) {
} }
} }
/**
* Indicates if this small video is currently visible.
*
* @return <tt>true</tt> if this small video isn't currently visible and
* <tt>false</tt> - otherwise.
*/
SmallVideo.prototype.isVisible = function () {
return $('#' + this.videoSpanId).is(':visible');
};
SmallVideo.prototype.showDisplayName = function(isShow) { SmallVideo.prototype.showDisplayName = function(isShow) {
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0); var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
if (isShow) { if (isShow) {
@ -57,7 +68,7 @@ SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) {
/** /**
* Sets the type of the video displayed by this instance. * Sets the type of the video displayed by this instance.
* @param videoType 'camera' or 'screen' * @param videoType 'camera' or 'desktop'
*/ */
SmallVideo.prototype.setVideoType = function (videoType) { SmallVideo.prototype.setVideoType = function (videoType) {
this.videoType = videoType; this.videoType = videoType;
@ -106,9 +117,10 @@ SmallVideo.prototype.setPresenceStatus = function (statusMsg) {
* Creates an audio or video element for a particular MediaStream. * Creates an audio or video element for a particular MediaStream.
*/ */
SmallVideo.createStreamElement = function (stream) { SmallVideo.createStreamElement = function (stream) {
var isVideo = stream.isVideoStream(); let isVideo = stream.isVideoTrack();
var element = isVideo ? document.createElement('video') let element = isVideo
? document.createElement('video')
: document.createElement('audio'); : document.createElement('audio');
if (isVideo) { if (isVideo) {
element.setAttribute("muted", "true"); element.setAttribute("muted", "true");
@ -118,8 +130,7 @@ SmallVideo.createStreamElement = function (stream) {
element.autoplay = true; element.autoplay = true;
} }
element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + element.id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
APP.RTC.getStreamID(stream.getOriginalStream());
element.onplay = function () { element.onplay = function () {
console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t", console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t",
@ -144,8 +155,8 @@ SmallVideo.prototype.bindHoverHandler = function () {
function () { function () {
// If the video has been "pinned" by the user we want to // If the video has been "pinned" by the user we want to
// keep the display name on place. // keep the display name on place.
if (!LargeVideo.isLargeVideoVisible() || if (!self.VideoLayout.isLargeVideoVisible() ||
!LargeVideo.isCurrentlyOnLarge(self.getResourceJid())) !self.VideoLayout.isCurrentlyOnLarge(self.id))
self.showDisplayName(false); self.showDisplayName(false);
} }
); );
@ -202,10 +213,12 @@ SmallVideo.prototype.showAudioIndicator = function(isMuted) {
}; };
/** /**
* Shows video muted indicator over small videos. * Shows video muted indicator over small videos and disables/enables avatar
* if video muted.
*/ */
SmallVideo.prototype.showVideoIndicator = function(isMuted) { SmallVideo.prototype.setMutedView = function(isMuted) {
this.showAvatar(isMuted); this.isVideoMuted = isMuted;
this.updateView();
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted'); var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
@ -232,43 +245,9 @@ SmallVideo.prototype.showVideoIndicator = function(isMuted) {
} }
this.updateIconPositions(); this.updateIconPositions();
} }
}; };
SmallVideo.prototype.enableDominantSpeaker = function (isEnable) {
var resourceJid = this.getResourceJid();
var displayName = resourceJid;
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
if (nameSpan.length > 0)
displayName = nameSpan.html();
console.log("UI enable dominant speaker",
displayName,
resourceJid,
isEnable);
if (!this.container) {
return;
}
if (isEnable) {
this.showDisplayName(LargeVideo.getState() === "video");
if (!this.container.classList.contains("dominantspeaker"))
this.container.classList.add("dominantspeaker");
}
else {
this.showDisplayName(false);
if (this.container.classList.contains("dominantspeaker"))
this.container.classList.remove("dominantspeaker");
}
this.showAvatar();
};
SmallVideo.prototype.updateIconPositions = function () { SmallVideo.prototype.updateIconPositions = function () {
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted'); var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator'); var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
@ -316,13 +295,15 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
}; };
SmallVideo.prototype.selectVideoElement = function () { SmallVideo.prototype.selectVideoElement = function () {
var videoElem = APP.RTC.getVideoElementName(); var videoElemName;
if (!RTCBrowserType.isTemasysPluginUsed()) { if (!RTCBrowserType.isTemasysPluginUsed()) {
return $('#' + this.videoSpanId).find(videoElem); videoElemName = 'video';
return $('#' + this.videoSpanId).find(videoElemName);
} else { } else {
videoElemName = 'object';
var matching = $('#' + this.videoSpanId + var matching = $('#' + this.videoSpanId +
(this.isLocal ? '>>' : '>') + (this.isLocal ? '>>' : '>') +
videoElem + '>param[value="video"]'); videoElemName + '>param[value="video"]');
if (matching.length < 2) { if (matching.length < 2) {
return matching.parent(); return matching.parent();
} }
@ -343,11 +324,6 @@ SmallVideo.prototype.selectVideoElement = function () {
} }
}; };
SmallVideo.prototype.getSrc = function () {
var videoElement = this.selectVideoElement().get(0);
return APP.RTC.getVideoSrc(videoElement);
};
SmallVideo.prototype.focus = function(isFocused) { SmallVideo.prototype.focus = function(isFocused) {
if(!isFocused) { if(!isFocused) {
this.container.classList.remove("videoContainerFocused"); this.container.classList.remove("videoContainerFocused");
@ -361,66 +337,76 @@ SmallVideo.prototype.hasVideo = function () {
}; };
/** /**
* Hides or shows the user's avatar * Hides or shows the user's avatar.
* This update assumes that large video had been updated and we will
* reflect it on this small video.
*
* @param show whether we should show the avatar or not * @param show whether we should show the avatar or not
* video because there is no dominant speaker and no focused speaker * video because there is no dominant speaker and no focused speaker
*/ */
SmallVideo.prototype.showAvatar = function (show) { SmallVideo.prototype.updateView = function () {
if (!this.hasAvatar) { if (!this.hasAvatar) {
if (this.peerJid) { if (this.id) {
// Init avatar // Init avatar
this.avatarChanged(Avatar.getAvatarUrl(this.peerJid)); this.avatarChanged(Avatar.getAvatarUrl(this.id));
} else { } else {
console.error("Unable to init avatar - no peerjid", this); console.error("Unable to init avatar - no id", this);
return; return;
} }
} }
var resourceJid = this.getResourceJid(); let video = this.selectVideoElement();
var video = this.selectVideoElement();
var avatar = $('#avatar_' + resourceJid); let avatar = $(`#avatar_${this.id}`);
if (show === undefined || show === null) { var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
if (!this.isLocal &&
!this.VideoLayout.isInLastN(resourceJid)) { var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
show = true; var showAvatar;
} else { if ((!this.isLocal
// We want to show the avatar when the video is muted or not exists && !this.VideoLayout.isInLastN(this.id))
// that is when 'true' or 'null' is returned || this.isVideoMuted) {
show = APP.RTC.isVideoMuted(this.peerJid) !== false; 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)) { showAvatar = showAvatar && !isCurrentlyOnLarge;
setVisibility(avatar, false);
setVisibility(video, false); if (video && video.length > 0) {
} else { setVisibility(video, showVideo);
if (video && video.length > 0) { }
setVisibility(video, !show); setVisibility(avatar, showAvatar);
}
setVisibility(avatar, show); var showDisplayName = !showVideo && !showAvatar;
if (showDisplayName) {
this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
}
else {
this.showDisplayName(false);
} }
}; };
SmallVideo.prototype.avatarChanged = function (thumbUrl) { SmallVideo.prototype.avatarChanged = function (avatarUrl) {
var thumbnail = $('#' + this.videoSpanId); var thumbnail = $('#' + this.videoSpanId);
var resourceJid = this.getResourceJid(); var avatar = $('#avatar_' + this.id);
var avatar = $('#avatar_' + resourceJid);
this.hasAvatar = true; this.hasAvatar = true;
// set the avatar in the thumbnail // set the avatar in the thumbnail
if (avatar && avatar.length > 0) { if (avatar && avatar.length > 0) {
avatar[0].src = thumbUrl; avatar[0].src = avatarUrl;
} else { } else {
if (thumbnail && thumbnail.length > 0) { if (thumbnail && thumbnail.length > 0) {
avatar = document.createElement('img'); avatar = document.createElement('img');
avatar.id = 'avatar_' + resourceJid; avatar.id = 'avatar_' + this.id;
avatar.className = 'userAvatar'; avatar.className = 'userAvatar';
avatar.src = thumbUrl; avatar.src = avatarUrl;
thumbnail.append(avatar); thumbnail.append(avatar);
} }
} }
}; };
module.exports = SmallVideo; export default SmallVideo;

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
var jssha = require('jssha'); var JSSHA = require('jssha');
module.exports = { module.exports = {
/** /**
@ -16,7 +16,7 @@ module.exports = {
// This implements the actual choice of an entry in the list based on // This implements the actual choice of an entry in the list based on
// roomName. Please consider the implications for existing deployments // roomName. Please consider the implications for existing deployments
// before introducing changes. // before introducing changes.
var hash = (new jssha(roomName, 'TEXT')).getHash('SHA-1', 'HEX'); var hash = (new JSSHA(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
var n = parseInt("0x"+hash.substr(-6)); var n = parseInt("0x"+hash.substr(-6));
var idx = n % config.boshList.length; var idx = n % config.boshList.length;
var attemptFirstAddress; var attemptFirstAddress;

View File

@ -3,7 +3,6 @@
var EventEmitter = require("events"); var EventEmitter = require("events");
var eventEmitter = new EventEmitter(); var eventEmitter = new EventEmitter();
var CQEvents = require("../../service/connectionquality/CQEvents"); var CQEvents = require("../../service/connectionquality/CQEvents");
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
var StatisticsEvents = require("../../service/statistics/Events"); var StatisticsEvents = require("../../service/statistics/Events");
/** /**
@ -18,69 +17,47 @@ var stats = {};
*/ */
var remoteStats = {}; var remoteStats = {};
/**
* Interval for sending statistics to other participants
* @type {null}
*/
var sendIntervalId = null;
/**
* Start statistics sending.
*/
function startSendingStats() {
sendStats();
sendIntervalId = setInterval(sendStats, 10000);
}
/**
* Sends statistics to other participants
*/
function sendStats() {
APP.xmpp.addToPresence("connectionQuality", convertToMUCStats(stats));
}
/**
* Converts statistics to format for sending through XMPP
* @param stats the statistics
* @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}}
*/
function convertToMUCStats(stats) {
return {
"bitrate_download": stats.bitrate.download,
"bitrate_upload": stats.bitrate.upload,
"packetLoss_total": stats.packetLoss.total,
"packetLoss_download": stats.packetLoss.download,
"packetLoss_upload": stats.packetLoss.upload
};
}
/** /**
* Converts statistics to format used by VideoLayout * Converts statistics to format used by VideoLayout
* @param stats * @param stats
* @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}} * @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
*/ */
function parseMUCStats(stats) { function parseMUCStats(stats) {
if(!stats || !stats.children || !stats.children.length)
return null;
var children = stats.children;
var extractedStats = {};
children.forEach((child) => {
if(child.tagName !== "stat" || !child.attributes)
return;
var attrKeys = Object.keys(child.attributes);
if(!attrKeys || !attrKeys.length)
return;
attrKeys.forEach((attr) => {
extractedStats[attr] = child.attributes[attr];
});
});
return { return {
bitrate: { bitrate: {
download: stats.bitrate_download, download: extractedStats.bitrate_download,
upload: stats.bitrate_upload upload: extractedStats.bitrate_upload
}, },
packetLoss: { packetLoss: {
total: stats.packetLoss_total, total: extractedStats.packetLoss_total,
download: stats.packetLoss_download, download: extractedStats.packetLoss_download,
upload: stats.packetLoss_upload upload: extractedStats.packetLoss_upload
} }
}; };
} }
var ConnectionQuality = { var ConnectionQuality = {
init: function () { init: function () {
APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats); APP.statistics.addListener(
APP.statistics.addListener(StatisticsEvents.CONNECTION_STATS, StatisticsEvents.CONNECTION_STATS, this.updateLocalStats
this.updateLocalStats); );
APP.statistics.addListener(StatisticsEvents.STOP, APP.statistics.addListener(
this.stopSendingStats); StatisticsEvents.STOP, this.stopSendingStats
);
}, },
/** /**
@ -90,33 +67,30 @@ var ConnectionQuality = {
updateLocalStats: function (data) { updateLocalStats: function (data) {
stats = data; stats = data;
eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats); eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats);
if (!sendIntervalId) {
startSendingStats();
}
}, },
/** /**
* Updates remote statistics * Updates remote statistics
* @param jid the jid associated with the statistics * @param id the id associated with the statistics
* @param data the statistics * @param data the statistics
*/ */
updateRemoteStats: function (jid, data) { updateRemoteStats: function (id, data) {
if (!data || !data.packetLoss_total) { data = parseMUCStats(data);
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null); if (!data || !data.packetLoss || !data.packetLoss.total) {
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, id, null, null);
return; return;
} }
remoteStats[jid] = parseMUCStats(data); remoteStats[id] = data;
eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, eventEmitter.emit(
jid, 100 - data.packetLoss_total, remoteStats[jid]); CQEvents.REMOTESTATS_UPDATED, id, 100 - data.packetLoss_total, remoteStats[id]
);
}, },
/** /**
* Stops statistics sending. * Stops statistics sending.
*/ */
stopSendingStats: function () { stopSendingStats: function () {
clearInterval(sendIntervalId);
sendIntervalId = null;
//notify UI about stopping statistics gathering //notify UI about stopping statistics gathering
eventEmitter.emit(CQEvents.STOP); eventEmitter.emit(CQEvents.STOP);
}, },
@ -127,11 +101,29 @@ var ConnectionQuality = {
getStats: function () { getStats: function () {
return stats; return stats;
}, },
addListener: function (type, listener) { addListener: function (type, listener) {
eventEmitter.on(type, listener); eventEmitter.on(type, listener);
} },
/**
* Converts statistics to format for sending through XMPP
* @param stats the statistics
* @returns [{tagName: "stat", attributes: {{bitrate_donwload: *}},
* {tagName: "stat", attributes: {{ bitrate_uplpoad: *}},
* {tagName: "stat", attributes: {{ packetLoss_total: *}},
* {tagName: "stat", attributes: {{ packetLoss_download: *}},
* {tagName: "stat", attributes: {{ packetLoss_upload: *}}]
*/
convertToMUCStats: function (stats) {
return [
{tagName: "stat", attributes: {"bitrate_download": stats.bitrate.download}},
{tagName: "stat", attributes: {"bitrate_upload": stats.bitrate.upload}},
{tagName: "stat", attributes: {"packetLoss_total": stats.packetLoss.total}},
{tagName: "stat", attributes: {"packetLoss_download": stats.packetLoss.download}},
{tagName: "stat", attributes: {"packetLoss_upload": stats.packetLoss.upload}}
];
}
}; };
module.exports = ConnectionQuality; module.exports = ConnectionQuality;

View File

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

View File

@ -1,10 +1,8 @@
/* global APP, config */ /* global APP, JitsiMeetJS, config */
var EventEmitter = require("events"); var EventEmitter = require("events");
var DesktopSharingEventTypes import DSEvents from '../../service/desktopsharing/DesktopSharingEventTypes';
= require("../../service/desktopsharing/DesktopSharingEventTypes");
var RTCBrowserType = require("../RTC/RTCBrowserType"); const TrackEvents = JitsiMeetJS.events.track;
var RTCEvents = require("../../service/RTC/RTCEvents");
var ScreenObtainer = require("./ScreenObtainer");
/** /**
* Indicates that desktop stream is currently in use (for toggle purpose). * Indicates that desktop stream is currently in use (for toggle purpose).
@ -20,22 +18,19 @@ var isUsingScreenStream = false;
var switchInProgress = false; var switchInProgress = false;
/** /**
* Used to obtain the screen sharing stream from the browser. * true if desktop sharing is enabled and false otherwise.
*/ */
var screenObtainer = new ScreenObtainer(); var isEnabled = false;
var eventEmitter = new EventEmitter(); var eventEmitter = new EventEmitter();
function streamSwitchDone() { function streamSwitchDone() {
switchInProgress = false; switchInProgress = false;
eventEmitter.emit( eventEmitter.emit(DSEvents.SWITCHING_DONE, isUsingScreenStream);
DesktopSharingEventTypes.SWITCHING_DONE,
isUsingScreenStream);
} }
function newStreamCreated(stream) { function newStreamCreated(track) {
eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED, eventEmitter.emit(DSEvents.NEW_STREAM_CREATED, track, streamSwitchDone);
stream, isUsingScreenStream, streamSwitchDone);
} }
function getVideoStreamFailed(error) { function getVideoStreamFailed(error) {
@ -50,36 +45,31 @@ function getDesktopStreamFailed(error) {
switchInProgress = false; switchInProgress = false;
} }
function onEndedHandler(stream) { function onEndedHandler() {
if (!switchInProgress && isUsingScreenStream) { if (!switchInProgress && isUsingScreenStream) {
APP.desktopsharing.toggleScreenSharing(); APP.desktopsharing.toggleScreenSharing();
} }
APP.RTC.removeMediaStreamInactiveHandler(stream, onEndedHandler);
} }
module.exports = { module.exports = {
isUsingScreenStream: function () { isUsingScreenStream: function () {
return isUsingScreenStream; return isUsingScreenStream;
}, },
/**
* Initializes the desktop sharing module.
* @param {boolean} <tt>true</tt> if desktop sharing feature is available
* and enabled.
*/
init: function (enabled) {
isEnabled = enabled;
},
/** /**
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available * @returns {boolean} <tt>true</tt> if desktop sharing feature is available
* and enabled. * and enabled.
*/ */
isDesktopSharingEnabled: function () { isDesktopSharingEnabled: function () {
return screenObtainer.isSupported(); return isEnabled;
}, },
init: function () {
// Called when RTC finishes initialization
APP.RTC.addListener(RTCEvents.RTC_READY,
function() {
screenObtainer.init(eventEmitter);
eventEmitter.emit(DesktopSharingEventTypes.INIT);
});
},
addListener: function (type, listener) { addListener: function (type, listener) {
eventEmitter.on(type, listener); eventEmitter.on(type, listener);
}, },
@ -95,43 +85,50 @@ module.exports = {
if (switchInProgress) { if (switchInProgress) {
console.warn("Switch in progress."); console.warn("Switch in progress.");
return; return;
} else if (!screenObtainer.isSupported()) { } else if (!this.isDesktopSharingEnabled()) {
console.warn("Cannot toggle screen sharing: not supported."); console.warn("Cannot toggle screen sharing: not supported.");
return; return;
} }
switchInProgress = true; switchInProgress = true;
let type;
if (!isUsingScreenStream) { if (!isUsingScreenStream) {
// Switch to desktop stream // Switch to desktop stream
screenObtainer.obtainStream( type = "desktop";
function (stream) {
// We now use screen stream
isUsingScreenStream = true;
// Hook 'ended' event to restore camera
// when screen stream stops
APP.RTC.addMediaStreamInactiveHandler(
stream, onEndedHandler);
newStreamCreated(stream);
},
getDesktopStreamFailed);
} else { } else {
// Disable screen stream type = "video";
APP.RTC.getUserMediaWithConstraints(
['video'],
function (stream) {
// We are now using camera stream
isUsingScreenStream = false;
newStreamCreated(stream);
},
getVideoStreamFailed,
config.resolution || '360'
);
} }
}, var fail = (error) => {
/* if (type === 'desktop') {
* Exports the event emitter to allow use by ScreenObtainer. Not for outside getDesktopStreamFailed(error);
* use. } else {
*/ getVideoStreamFailed(error);
eventEmitter: eventEmitter }
}; };
APP.conference.createLocalTracks(type).then((tracks) => {
// FIXME does it mean that 'not track.length' == GUM failed ?
// And will this ever happen if promise is supposed to fail in GUM
// failed case ?
if (!tracks.length) {
fail();
return;
}
let stream = tracks[0];
// We now use screen stream
isUsingScreenStream = type === "desktop";
if (isUsingScreenStream) {
stream.on(TrackEvents.TRACK_STOPPED, onEndedHandler);
}
newStreamCreated(stream);
}).catch((error) => {
if(error === JitsiMeetJS.errors.track.FIREFOX_EXTENSION_NEEDED)
{
eventEmitter.emit(
DSEvents.FIREFOX_EXTENSION_NEEDED,
config.desktopSharingFirefoxExtensionURL);
return;
}
fail(error);
});
}
};

View File

@ -21,20 +21,18 @@ function initShortcutHandlers() {
77: { 77: {
character: "M", character: "M",
id: "mutePopover", id: "mutePopover",
function: APP.UI.toggleAudio function: APP.conference.toggleAudioMuted
}, },
84: { 84: {
character: "T", character: "T",
function: function() { function: function() {
if(!APP.RTC.localAudio.isMuted()) { APP.conference.muteAudio(true);
APP.UI.toggleAudio();
}
} }
}, },
86: { 86: {
character: "V", character: "V",
id: "toggleVideoPopover", id: "toggleVideoPopover",
function: APP.UI.toggleVideo function: APP.conference.toggleVideoMuted
} }
}; };
} }
@ -67,9 +65,7 @@ var KeyboardShortcut = {
$(":focus").is("input[type=password]") || $(":focus").is("input[type=password]") ||
$(":focus").is("textarea"))) { $(":focus").is("textarea"))) {
if(e.which === "T".charCodeAt(0)) { if(e.which === "T".charCodeAt(0)) {
if(APP.RTC.localAudio.isMuted()) { APP.conference.muteAudio(true);
APP.UI.toggleAudio();
}
} }
} }
}; };

View File

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

View File

@ -1,11 +1,9 @@
var UsernameGenerator = require('../util/UsernameGenerator'); import {generateUsername} from '../util/UsernameGenerator';
var email = ''; var email = '';
var displayName = ''; var displayName = '';
var userId; var userId;
var language = null; var language = null;
var callStatsUserName;
function supportsLocalStorage() { function supportsLocalStorage() {
try { try {
@ -30,25 +28,16 @@ if (supportsLocalStorage()) {
console.log("generated id", window.localStorage.jitsiMeetId); console.log("generated id", window.localStorage.jitsiMeetId);
} }
if (!window.localStorage.callStatsUserName) {
window.localStorage.callStatsUserName
= UsernameGenerator.generateUsername();
console.log('generated callstats uid',
window.localStorage.callStatsUserName);
}
userId = window.localStorage.jitsiMeetId || ''; userId = window.localStorage.jitsiMeetId || '';
callStatsUserName = window.localStorage.callStatsUserName;
email = window.localStorage.email || ''; email = window.localStorage.email || '';
displayName = window.localStorage.displayname || ''; displayName = window.localStorage.displayname || '';
language = window.localStorage.language; language = window.localStorage.language;
} else { } else {
console.log("local storage is not supported"); console.log("local storage is not supported");
userId = generateUniqueId(); userId = generateUniqueId();
callStatsUserName = UsernameGenerator.generateUsername();
} }
var Settings = { export default {
/** /**
* Sets the local user display name and saves it to local storage * Sets the local user display name and saves it to local storage
@ -57,6 +46,9 @@ var Settings = {
* @returns {string} the display name we just set * @returns {string} the display name we just set
*/ */
setDisplayName: function (newDisplayName) { setDisplayName: function (newDisplayName) {
if (displayName === newDisplayName) {
return displayName;
}
displayName = newDisplayName; displayName = newDisplayName;
window.localStorage.displayname = displayName; window.localStorage.displayname = displayName;
return displayName; return displayName;
@ -70,20 +62,16 @@ var Settings = {
return displayName; return displayName;
}, },
/**
* Returns fake username for callstats
* @returns {string} fake username for callstats
*/
getCallStatsUserName: function () {
return callStatsUserName;
},
setEmail: function (newEmail) { setEmail: function (newEmail) {
email = newEmail; email = newEmail;
window.localStorage.email = newEmail; window.localStorage.email = newEmail;
return email; return email;
}, },
getEmail: function () {
return email;
},
getSettings: function () { getSettings: function () {
return { return {
email: email, email: email,
@ -92,10 +80,11 @@ var Settings = {
language: language language: language
}; };
}, },
getLanguage () {
return language;
},
setLanguage: function (lang) { setLanguage: function (lang) {
language = lang; language = lang;
window.localStorage.language = lang; window.localStorage.language = lang;
} }
}; };
module.exports = Settings;

View File

@ -1,6 +1,4 @@
/* global config */ /* global config JitsiMeetJS */
var ScriptUtil = require('../util/ScriptUtil');
// Load the integration of a third-party analytics API such as Google Analytics. // Load the integration of a third-party analytics API such as Google Analytics.
// Since we cannot guarantee the quality of the third-party service (e.g. their // Since we cannot guarantee the quality of the third-party service (e.g. their
@ -11,37 +9,38 @@ var ScriptUtil = require('../util/ScriptUtil');
// its implementation asynchronously anyway so it makes sense to append the // its implementation asynchronously anyway so it makes sense to append the
// loading on our side rather than prepend it. // loading on our side rather than prepend it.
if (config.disableThirdPartyRequests !== true) { if (config.disableThirdPartyRequests !== true) {
ScriptUtil.loadScript( JitsiMeetJS.util.ScriptUtil.loadScript(
'analytics.js?v=1', 'analytics.js?v=1',
/* async */ true, /* async */ true,
/* prepend */ false); /* prepend */ false);
} }
// NoopAnalytics class NoopAnalytics {
function NoopAnalytics() {} sendEvent () {}
NoopAnalytics.prototype.sendEvent = function () {};
// AnalyticsAdapter
function AnalyticsAdapter() {
// XXX Since we asynchronously load the integration of the analytics API and
// the analytics API may asynchronously load its implementation (e.g. Google
// Analytics), we cannot make the decision with respect to which analytics
// implementation we will use here and we have to postpone it i.e. we will
// make a lazy decision.
} }
AnalyticsAdapter.prototype.sendEvent = function (action, data) { // XXX Since we asynchronously load the integration of the analytics API and the
var a = this.analytics; // 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') { class AnalyticsAdapter {
var AnalyticsImpl = window.Analytics || NoopAnalytics; constructor () {
}
this.analytics = a = new AnalyticsImpl(); sendEvent (...args) {
} var a = this.analytics;
try {
a.sendEvent.apply(a, arguments);
} catch (ignored) {}
};
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();

View File

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

View File

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

View File

@ -146,7 +146,6 @@ function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, even
{ {
this.peerconnection = peerconnection; this.peerconnection = peerconnection;
this.baselineAudioLevelsReport = null; this.baselineAudioLevelsReport = null;
this.currentAudioLevelsReport = null;
this.currentStatsReport = null; this.currentStatsReport = null;
this.baselineStatsReport = null; this.baselineStatsReport = null;
this.audioLevelsIntervalId = null; this.audioLevelsIntervalId = null;
@ -252,10 +251,7 @@ StatsCollector.prototype.start = function ()
results = report.result(); results = report.result();
} }
//console.error("Got interval report", results); //console.error("Got interval report", results);
self.currentAudioLevelsReport = results; self.baselineAudioLevelsReport = results;
self.processAudioLevelReport();
self.baselineAudioLevelsReport =
self.currentAudioLevelsReport;
}, },
self.errorCallback self.errorCallback
); );
@ -385,7 +381,7 @@ StatsCollector.prototype.addStatsToBeLogged = function (reports) {
StatsCollector.prototype.logStats = function () { StatsCollector.prototype.logStats = function () {
if(!APP.xmpp.sendLogs(this.statsToBeLogged)) if(!APP.conference._room.xmpp.sendLogs(this.statsToBeLogged))
return; return;
// Reset the stats // Reset the stats
this.statsToBeLogged.stats = {}; this.statsToBeLogged.stats = {};
@ -501,7 +497,7 @@ StatsCollector.prototype.processStatsReport = function () {
var ssrc = getStatValue(now, 'ssrc'); var ssrc = getStatValue(now, 'ssrc');
if(!ssrc) if(!ssrc)
continue; continue;
var jid = APP.xmpp.getJidFromSSRC(ssrc); var jid = APP.conference._room.room.getJidBySSRC(ssrc);
if (!jid && (Date.now() - now.timestamp) < 3000) { if (!jid && (Date.now() - now.timestamp) < 3000) {
console.warn("No jid for ssrc: " + ssrc); console.warn("No jid for ssrc: " + ssrc);
continue; continue;
@ -647,76 +643,22 @@ StatsCollector.prototype.processStatsReport = function () {
upload: upload:
calculatePacketLoss(lostPackets.upload, totalPackets.upload) calculatePacketLoss(lostPackets.upload, totalPackets.upload)
}; };
let idResolution = {};
if (resolutions) { // use id instead of jid
Object.keys(resolutions).forEach(function (jid) {
let id = Strophe.getResourceFromJid(jid);
idResolution[id] = resolutions[jid];
});
}
this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS, this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS,
{ {
"bitrate": PeerStats.bitrate, "bitrate": PeerStats.bitrate,
"packetLoss": PeerStats.packetLoss, "packetLoss": PeerStats.packetLoss,
"bandwidth": PeerStats.bandwidth, "bandwidth": PeerStats.bandwidth,
"resolution": resolutions, "resolution": idResolution,
"transport": PeerStats.transport "transport": PeerStats.transport
}); });
PeerStats.transport = []; PeerStats.transport = [];
}; };
/**
* Stats processing logic.
*/
StatsCollector.prototype.processAudioLevelReport = function () {
if (!this.baselineAudioLevelsReport) {
return;
}
for (var idx in this.currentAudioLevelsReport) {
var now = this.currentAudioLevelsReport[idx];
if (now.type != 'ssrc') {
continue;
}
var before = this.baselineAudioLevelsReport[idx];
if (!before) {
console.warn(getStatValue(now, 'ssrc') + ' not enough data');
continue;
}
var ssrc = getStatValue(now, 'ssrc');
var jid = APP.xmpp.getJidFromSSRC(ssrc);
if (!jid) {
if((Date.now() - now.timestamp) < 3000)
console.warn("No jid for ssrc: " + ssrc);
continue;
}
var jidStats = this.jid2stats[jid];
if (!jidStats) {
jidStats = new PeerStats();
this.jid2stats[jid] = jidStats;
}
// Audio level
var audioLevel = null;
try {
audioLevel = getStatValue(now, 'audioInputLevel');
if (!audioLevel)
audioLevel = getStatValue(now, 'audioOutputLevel');
}
catch(e) {/*not supported*/
console.warn("Audio Levels are not available in the statistics.");
clearInterval(this.audioLevelsIntervalId);
return;
}
if (audioLevel) {
// TODO: can't find specs about what this value really is,
// but it seems to vary between 0 and around 32k.
audioLevel = audioLevel / 32767;
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
if (jid != APP.xmpp.myJid()) {
this.eventEmitter.emit(
StatisticsEvents.AUDIO_LEVEL, jid, audioLevel);
}
}
}
};

View File

@ -2,28 +2,17 @@
/** /**
* Created by hristo on 8/4/14. * Created by hristo on 8/4/14.
*/ */
var LocalStats = require("./LocalStatsCollector.js");
var RTPStats = require("./RTPStatsCollector.js"); var RTPStats = require("./RTPStatsCollector.js");
var EventEmitter = require("events"); var EventEmitter = require("events");
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js"); var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var XMPPEvents = require("../../service/xmpp/XMPPEvents");
var CallStats = require("./CallStats");
var RTCEvents = require("../../service/RTC/RTCEvents"); var RTCEvents = require("../../service/RTC/RTCEvents");
var StatisticsEvents = require("../../service/statistics/Events"); var StatisticsEvents = require("../../service/statistics/Events");
var eventEmitter = new EventEmitter(); var eventEmitter = new EventEmitter();
var localStats = null;
var rtpStats = null; var rtpStats = null;
function stopLocal() {
if (localStats) {
localStats.stop();
localStats = null;
}
}
function stopRemote() { function stopRemote() {
if (rtpStats) { if (rtpStats) {
rtpStats.stop(); rtpStats.stop();
@ -41,26 +30,14 @@ function startRemoteStats (peerconnection) {
rtpStats.start(); rtpStats.start();
} }
function onStreamCreated(stream) {
if(stream.getOriginalStream().getAudioTracks().length === 0) {
return;
}
localStats = new LocalStats(stream.getOriginalStream(), 200, statistics,
eventEmitter);
localStats.start();
}
function onDisposeConference(onUnload) { function onDisposeConference(onUnload) {
CallStats.sendTerminateEvent();
stopRemote(); stopRemote();
if(onUnload) { if (onUnload) {
stopLocal();
eventEmitter.removeAllListeners(); eventEmitter.removeAllListeners();
} }
} }
var statistics = { export default {
/** /**
* Indicates that this audio level is for local jid. * Indicates that this audio level is for local jid.
* @type {string} * @type {string}
@ -74,89 +51,21 @@ var statistics = {
eventEmitter.removeListener(type, listener); eventEmitter.removeListener(type, listener);
}, },
stop: function () { stop: function () {
stopLocal();
stopRemote(); stopRemote();
if(eventEmitter) if (eventEmitter) {
{
eventEmitter.removeAllListeners(); eventEmitter.removeAllListeners();
} }
}, },
stopRemoteStatistics: function()
{
stopRemote();
},
start: function () { start: function () {
APP.RTC.addStreamListener(onStreamCreated, const xmpp = APP.conference._room.xmpp;
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED); xmpp.addListener(
APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, XMPPEvents.DISPOSE_CONFERENCE,
onDisposeConference); onDisposeConference
);
//FIXME: we may want to change CALL INCOMING event to //FIXME: we may want to change CALL INCOMING event to
// onnegotiationneeded // onnegotiationneeded
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) { xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
startRemoteStats(event.peerconnection); startRemoteStats(event.peerconnection);
// CallStats.init(event);
}); });
APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY,
function (session) {
CallStats.init(session);
});
APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) {
CallStats.sendMuteEvent(mute, "audio");
});
APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
CallStats.sendSetupFailedEvent();
});
APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) {
CallStats.sendMuteEvent(mute, "video");
});
APP.RTC.addListener(RTCEvents.GET_USER_MEDIA_FAILED, function (e) {
CallStats.sendGetUserMediaFailed(e);
});
APP.xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e, pc) {
CallStats.sendCreateOfferFailed(e, pc);
});
APP.xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e, pc) {
CallStats.sendCreateAnswerFailed(e, pc);
});
APP.xmpp.addListener(
RTCEvents.SET_LOCAL_DESCRIPTION_FAILED,
function (e, pc) {
CallStats.sendSetLocalDescFailed(e, pc);
}
);
APP.xmpp.addListener(
RTCEvents.SET_REMOTE_DESCRIPTION_FAILED,
function (e, pc) {
CallStats.sendSetRemoteDescFailed(e, pc);
}
);
APP.xmpp.addListener(
RTCEvents.ADD_ICE_CANDIDATE_FAILED,
function (e, pc) {
CallStats.sendAddIceCandidateFailed(e, pc);
}
);
},
/**
* Obtains audio level reported in the stats for specified peer.
* @param peerJid full MUC jid of the user for whom we want to obtain last
* audio level.
* @param ssrc the SSRC of audio stream for which we want to obtain audio
* level.
* @returns {*} a float form 0 to 1 that represents current audio level or
* <tt>null</tt> if for any reason the value is not available
* at this time.
*/
getPeerSSRCAudioLevel: function (peerJid, ssrc) {
var peerStats = rtpStats.jid2stats[peerJid];
return peerStats ? peerStats.ssrc2AudioLevel[ssrc] : null;
} }
}; };
module.exports = statistics;

View File

@ -1,7 +1,6 @@
/* global $, require, config, interfaceConfig */ /* global $, require, config, interfaceConfig */
var i18n = require("i18next-client"); var i18n = require("i18next-client");
var languages = require("../../service/translation/languages"); var languages = require("../../service/translation/languages");
var Settings = require("../settings/Settings");
var DEFAULT_LANG = languages.EN; var DEFAULT_LANG = languages.EN;
i18n.addPostProcessor("resolveAppName", function(value, key, options) { i18n.addPostProcessor("resolveAppName", function(value, key, options) {
@ -68,7 +67,7 @@ function initCompleted(t) {
$("[data-i18n]").i18n(); $("[data-i18n]").i18n();
} }
function checkForParameter() { function getLangFromQuery() {
var query = window.location.search.substring(1); var query = window.location.search.substring(1);
var vars = query.split("&"); var vars = query.split("&");
for (var i=0;i<vars.length;i++) { for (var i=0;i<vars.length;i++) {
@ -82,27 +81,11 @@ function checkForParameter() {
} }
module.exports = { module.exports = {
init: function (lang) { init: function (settingsLang) {
var options = defaultOptions; let options = defaultOptions;
let lang = getLangFromQuery() || settingsLang || config.defaultLanguage;
if(!lang) if (lang) {
{
lang = checkForParameter();
if(!lang)
{
var settings = Settings.getSettings();
if(settings)
lang = settings.language;
if(!lang && config.defaultLanguage)
{
lang = config.defaultLanguage;
}
}
}
if(lang) {
options.lng = lang; options.lng = lang;
} }
@ -124,8 +107,7 @@ module.exports = {
}, },
generateTranslationHTML: function (key, options) { generateTranslationHTML: function (key, options) {
var str = "<span data-i18n=\"" + key + "\""; var str = "<span data-i18n=\"" + key + "\"";
if(options) if (options) {
{
str += " data-i18n-options=\"" + JSON.stringify(options) + "\""; str += " data-i18n-options=\"" + JSON.stringify(options) + "\"";
} }
str += ">"; str += ">";

View File

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

View File

@ -1,4 +1,4 @@
var RandomUtil = require('./RandomUtil'); import RandomUtil from './RandomUtil';
/** /**
* from faker.js - Copyright (c) 2014-2015 Matthew Bergman & Marak Squires * from faker.js - Copyright (c) 2014-2015 Matthew Bergman & Marak Squires
@ -417,13 +417,9 @@ var names = [
* Generate random username. * Generate random username.
* @returns {string} random username * @returns {string} random username
*/ */
function generateUsername () { export function generateUsername () {
var name = RandomUtil.randomElement(names); var name = RandomUtil.randomElement(names);
var suffix = RandomUtil.randomAlphanumStr(3); var suffix = RandomUtil.randomAlphanumStr(3);
return name + '-' + suffix; return name + '-' + suffix;
} }
module.exports = {
generateUsername: generateUsername
};

14
modules/util/helpers.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,10 @@
"jshint": "2.8.0", "jshint": "2.8.0",
"precommit-hook": "3.0.0", "precommit-hook": "3.0.0",
"uglify-js": "2.4.24", "uglify-js": "2.4.24",
"clean-css": "*" "clean-css": "*",
"babelify": "*",
"babel-preset-es2015": "*",
"babel-polyfill": "*"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
@ -54,9 +57,15 @@
], ],
"browserify": { "browserify": {
"transform": [ "transform": [
"browserify-shim" "browserify-shim",
["babelify", {
"ignore": "node_modules"
}]
] ]
}, },
"babel": {
"presets": ["es2015"]
},
"browser": { "browser": {
"jquery": "./node_modules/jquery/dist/jquery.js", "jquery": "./node_modules/jquery/dist/jquery.js",
"jquery-ui": "./node_modules/jquery-ui/jquery-ui.js", "jquery-ui": "./node_modules/jquery-ui/jquery-ui.js",

View File

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

View File

@ -1,6 +0,0 @@
var MediaStreamType = {
VIDEO_TYPE: "video",
AUDIO_TYPE: "audio"
};
module.exports = MediaStreamType;

View File

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

View File

@ -1,12 +1,45 @@
var UIEvents = { export default {
NICKNAME_CHANGED: "UI.nickname_changed", NICKNAME_CHANGED: "UI.nickname_changed",
SELECTED_ENDPOINT: "UI.selected_endpoint", SELECTED_ENDPOINT: "UI.selected_endpoint",
PINNED_ENDPOINT: "UI.pinned_endpoint", PINNED_ENDPOINT: "UI.pinned_endpoint",
LARGEVIDEO_INIT: "UI.largevideo_init",
/** /**
* Notifies interested parties when the film strip (remote video's panel) * Notifies that local user created text message.
* is hidden (toggled) or shown (un-toggled).
*/ */
FILM_STRIP_TOGGLED: "UI.filmstrip_toggled" MESSAGE_CREATED: "UI.message_created",
/**
* Notifies that local user changed language.
*/
LANG_CHANGED: "UI.lang_changed",
/**
* Notifies that local user changed email.
*/
EMAIL_CHANGED: "UI.email_changed",
/**
* Notifies that "start muted" settings changed.
*/
START_MUTED_CHANGED: "UI.start_muted_changed",
AUDIO_MUTED: "UI.audio_muted",
VIDEO_MUTED: "UI.video_muted",
PREZI_CLICKED: "UI.prezi_clicked",
SHARE_PREZI: "UI.share_prezi",
PREZI_SLIDE_CHANGED: "UI.prezi_slide_changed",
STOP_SHARING_PREZI: "UI.stop_sharing_prezi",
ETHERPAD_CLICKED: "UI.etherpad_clicked",
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
USER_INVITED: "UI.user_invited",
USER_KICKED: "UI.user_kicked",
REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
AUTH_CLICKED: "UI.auth_clicked",
TOGGLE_CHAT: "UI.toggle_chat",
TOGGLE_SETTINGS: "UI.toggle_settings",
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
TOGGLE_FILM_STRIP: "UI.toggle_film_strip",
TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
CONTACT_CLICKED: "UI.contact_clicked",
HANGUP: "UI.hangup",
LOGOUT: "UI.logout",
RECORDING_TOGGLE: "UI.recording_toggle",
SIP_DIAL: "UI.sip_dial",
SUBEJCT_CHANGED: "UI.subject_changed"
}; };
module.exports = UIEvents;

View File

@ -1,6 +1,4 @@
var DesktopSharingEventTypes = { export default {
INIT: "ds.init",
SWITCHING_DONE: "ds.switching_done", SWITCHING_DONE: "ds.switching_done",
NEW_STREAM_CREATED: "ds.new_stream_created", NEW_STREAM_CREATED: "ds.new_stream_created",
@ -11,5 +9,3 @@ var DesktopSharingEventTypes = {
*/ */
FIREFOX_EXTENSION_NEEDED: "ds.firefox_extension_needed" FIREFOX_EXTENSION_NEEDED: "ds.firefox_extension_needed"
}; };
module.exports = DesktopSharingEventTypes;

View File

@ -1,5 +0,0 @@
var Events = {
DTMF_SUPPORT_CHANGED: "members.dtmf_support_changed"
};
module.exports = Events;

View File

@ -86,8 +86,6 @@ var XMPPEvents = {
JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error', JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error',
PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login', PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
FOCUS_DISCONNECTED: 'xmpp.focus_disconnected', FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
ROOM_JOIN_ERROR: 'xmpp.room_join_error',
ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
// xmpp is connected and obtained user media // xmpp is connected and obtained user media
READY_TO_JOIN: 'xmpp.ready_to_join' READY_TO_JOIN: 'xmpp.ready_to_join'
}; };