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
lib-jitsi-meet.js -text -diff

View File

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

View File

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

View File

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

122
app.js
View File

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

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

2
debian/control vendored
View File

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

View File

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

View File

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

View File

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

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

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

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 */
var CanvasUtil = require("./CanvasUtils");
var ASDrawContext = null;
import CanvasUtil from './CanvasUtils';
import BottomToolbar from '../toolbars/BottomToolbar';
function initActiveSpeakerAudioLevels() {
var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
const LOCAL_LEVEL = 'local';
let ASDrawContext = null;
let audioLevelCanvasCache = {};
function initDominantSpeakerAudioLevels() {
let ASRadius = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE / 2;
let ASCenter = (interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
// Draw a circle.
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
@ -17,250 +22,214 @@ function initActiveSpeakerAudioLevels() {
ASDrawContext.shadowOffsetY = 0;
}
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(id, audioLevel) {
if (!audioLevelCanvasCache[id]) {
let videoSpanId = getVideoSpanId(id);
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig) {
audioLevelCanvasCache[id] = CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
let canvas = audioLevelCanvasCache[id];
if (!canvas) {
return;
}
let drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
let shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0) {
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow(drawContext,
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
}
}
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
let shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
} else if (audioLevel <= 0.6) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
} else {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
}
/**
* Returns the video span id corresponding to the given user id
*/
function getVideoSpanId(id) {
let videoSpanId = null;
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
videoSpanId = 'localVideoContainer';
} else {
videoSpanId = `participant_${id}`;
}
return videoSpanId;
}
/**
* The audio Levels plugin.
*/
var AudioLevels = (function(my) {
var audioLevelCanvasCache = {};
const AudioLevels = {
my.LOCAL_LEVEL = 'local';
my.init = function () {
ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d');
initActiveSpeakerAudioLevels();
};
init () {
ASDrawContext = $('#dominantSpeakerAudioLevel')[0].getContext('2d');
initDominantSpeakerAudioLevels();
},
/**
* Updates the audio level canvas for the given peerJid. If the canvas
* Updates the audio level canvas for the given id. If the canvas
* didn't exist we create it.
*/
my.updateAudioLevelCanvas = function (peerJid, VideoLayout) {
var resourceJid = null;
var videoSpanId = null;
if (!peerJid)
videoSpanId = 'localVideoContainer';
else {
resourceJid = Strophe.getResourceFromJid(peerJid);
videoSpanId = 'participant_' + resourceJid;
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
let videoSpanId = 'localVideoContainer';
if (id) {
videoSpanId = `participant_${id}`;
}
var videoSpan = document.getElementById(videoSpanId);
let videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
if (resourceJid)
console.error("No video element for jid", resourceJid);
else
if (id) {
console.error("No video element for id", id);
} else {
console.error("No video element for local video.");
}
return;
}
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var thumbnailWidth = thumbnailSize[0];
var thumbnailHeight = thumbnailSize[1];
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
audioLevelCanvas.style.bottom = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
audioLevelCanvas.style.left = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
}
};
},
/**
* Updates the audio level UI for the given resourceJid.
* Updates the audio level UI for the given id.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param id id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) {
drawAudioLevelCanvas(resourceJid, audioLevel);
updateAudioLevel (id, audioLevel, largeVideoId) {
drawAudioLevelCanvas(id, audioLevel);
var videoSpanId = getVideoSpanId(resourceJid);
let videoSpanId = getVideoSpanId(id);
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
if (!audioLevelCanvas)
if (!audioLevelCanvas) {
return;
}
var drawContext = audioLevelCanvas.getContext('2d');
let drawContext = audioLevelCanvas.getContext('2d');
var canvasCache = audioLevelCanvasCache[resourceJid];
let canvasCache = audioLevelCanvasCache[id];
drawContext.clearRect (0, 0,
audioLevelCanvas.width, audioLevelCanvas.height);
drawContext.clearRect(
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
);
drawContext.drawImage(canvasCache, 0, 0);
if(resourceJid === AudioLevels.LOCAL_LEVEL) {
if(!APP.xmpp.myJid()) {
if (id === LOCAL_LEVEL) {
id = APP.conference.localId;
if (!id) {
return;
}
resourceJid = APP.xmpp.myResource();
}
if(resourceJid === largeVideoResourceJid) {
if(id === largeVideoId) {
window.requestAnimationFrame(function () {
AudioLevels.updateActiveSpeakerAudioLevel(audioLevel);
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
});
}
};
},
my.updateActiveSpeakerAudioLevel = function(audioLevel) {
if($("#activeSpeaker").css("visibility") == "hidden" || ASDrawContext === null)
updateDominantSpeakerAudioLevel (audioLevel) {
if($("#domiantSpeaker").css("visibility") == "hidden" || ASDrawContext === null) {
return;
}
ASDrawContext.clearRect(0, 0, 300, 300);
if (!audioLevel)
if (!audioLevel) {
return;
}
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
// Fill the shape.
ASDrawContext.fill();
};
},
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
updateCanvasSize (thumbWidth, thumbHeight) {
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(resourceJid, audioLevel) {
if (!audioLevelCanvasCache[resourceJid]) {
BottomToolbar.getThumbs().children('canvas').width(canvasWidth).height(canvasHeight);
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig) {
audioLevelCanvasCache[resourceJid] =
CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
var canvas = audioLevelCanvasCache[resourceJid];
if (!canvas)
return;
var drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
var shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0) {
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow(drawContext,
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
}
}
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
var shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
}
else if (audioLevel <= 0.6) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
}
else {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
}
/**
* Returns the video span id corresponding to the given resourceJid or local
* user.
*/
function getVideoSpanId(resourceJid) {
var videoSpanId = null;
if (resourceJid === AudioLevels.LOCAL_LEVEL ||
(APP.xmpp.myResource() && resourceJid === APP.xmpp.myResource()))
videoSpanId = 'localVideoContainer';
else
videoSpanId = 'participant_' + resourceJid;
return videoSpanId;
}
/**
* Indicates that the remote video has been resized.
*/
$(document).bind('remotevideo.resized', function (event, width, height) {
var resized = false;
$('#remoteVideos>span>canvas').each(function() {
var canvas = $(this).get(0);
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) {
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
Object.keys(audioLevelCanvasCache).forEach(function (id) {
audioLevelCanvasCache[id].width = canvasWidth;
audioLevelCanvasCache[id].height = canvasHeight;
});
}
};
if (resized)
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
audioLevelCanvasCache[resourceJid].width =
width + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvasCache[resourceJid].height =
height + interfaceConfig.CANVAS_EXTRA;
});
});
return my;
})(AudioLevels || {});
module.exports = AudioLevels;
export default AudioLevels;

View File

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

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

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 */
var Settings = require("../../settings/Settings");
/* global MD5, config, interfaceConfig */
var users = {};
let users = {};
var Avatar = {
export default {
/**
* Sets the user's avatar in the settings menu(if local user), contact list
* and thumbnail
* @param jid jid of the user
* @param id email or userID to be used as a hash
* @param id id of the user
* @param email email or nickname to be used as a hash
*/
setUserAvatar: function (jid, id) {
if (id) {
if (users[jid] === id) {
setUserAvatar: function (id, email) {
if (email) {
if (users[id] === email) {
return;
}
users[jid] = id;
users[id] = email;
}
var avatarUrl = this.getAvatarUrl(jid);
var resourceJid = Strophe.getResourceFromJid(jid);
APP.UI.userAvatarChanged(resourceJid, avatarUrl);
},
/**
* Returns the URL of the image for the avatar of a particular user,
* identified by its jid
* @param jid
* @param jid full MUC jid of the user for whom we want to obtain avatar URL
* identified by its id.
* @param {string} userId user id
*/
getAvatarUrl: function (jid) {
getAvatarUrl: function (userId) {
if (config.disableThirdPartyRequests) {
return 'images/avatar2.png';
} else {
if (!jid) {
console.error("Get avatar - jid is undefined");
return null;
}
var id = users[jid];
// If the ID looks like an email, we'll use gravatar.
// Otherwise, it's a random avatar, and we'll use the configured
// URL.
var random = !id || id.indexOf('@') < 0;
if (!id) {
console.warn(
"No avatar stored yet for " + jid + " - using JID as ID");
id = jid;
}
id = MD5.hexdigest(id.trim().toLowerCase());
// Default to using gravatar.
var urlPref = 'https://www.gravatar.com/avatar/';
var urlSuf = "?d=wavatar&size=100";
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
}
return urlPref + id + urlSuf;
}
if (!userId) {
console.error("Get avatar - id is undefined");
return null;
}
let avatarId = users[userId];
// If the ID looks like an email, we'll use gravatar.
// Otherwise, it's a random avatar, and we'll use the configured
// URL.
let random = !avatarId || avatarId.indexOf('@') < 0;
if (!avatarId) {
console.warn(
`No avatar stored yet for ${userId} - using ID as avatar ID`);
avatarId = userId;
}
avatarId = MD5.hexdigest(avatarId.trim().toLowerCase());
// Default to using gravatar.
let urlPref = 'https://www.gravatar.com/avatar/';
let urlSuf = "?d=wavatar&size=100";
if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
}
return urlPref + avatarId + urlSuf;
}
};
module.exports = Avatar;

View File

@ -1,61 +1,20 @@
/* global $, config,
setLargeVideoVisible, Util */
var VideoLayout = require("../videolayout/VideoLayout");
var Prezi = require("../prezi/Prezi");
var UIUtil = require("../util/UIUtil");
var etherpadName = null;
var etherpadIFrame = null;
var domain = null;
var options = "?showControls=true&showChat=false&showLineNumbers=true" +
"&useMonospaceFont=false";
/* global $ */
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import BottomToolbar from '../toolbars/BottomToolbar';
/**
* Resizes the etherpad.
* Etherpad options.
*/
function resize() {
if ($('#etherpad>iframe').length) {
var remoteVideos = $('#remoteVideos');
var availableHeight
= window.innerHeight - remoteVideos.outerHeight();
var availableWidth = UIUtil.getAvailableVideoWidth();
$('#etherpad>iframe').width(availableWidth);
$('#etherpad>iframe').height(availableHeight);
}
}
/**
* Creates the Etherpad button and adds it to the toolbar.
*/
function enableEtherpadButton() {
if (!$('#toolbar_button_etherpad').is(":visible"))
$('#toolbar_button_etherpad').css({display: 'inline-block'});
}
/**
* Creates the IFrame for the etherpad.
*/
function createIFrame() {
etherpadIFrame = VideoLayout.createEtherpadIframe(
domain + etherpadName + options, function() {
document.domain = document.domain;
bubbleIframeMouseMove(etherpadIFrame);
setTimeout(function() {
// the iframes inside of the etherpad are
// not yet loaded when the etherpad iframe is loaded
var outer = etherpadIFrame.
contentDocument.getElementsByName("ace_outer")[0];
bubbleIframeMouseMove(outer);
var inner = outer.
contentDocument.getElementsByName("ace_inner")[0];
bubbleIframeMouseMove(inner);
}, 2000);
});
}
const options = $.param({
showControns: true,
showChat: false,
showLineNumbers: true,
useMonospaceFont: false
});
function bubbleIframeMouseMove(iframe){
var existingOnMouseMove = iframe.contentWindow.onmousemove;
@ -71,8 +30,8 @@ function bubbleIframeMouseMove(iframe){
e.detail,
e.screenX,
e.screenY,
e.clientX + boundingClientRect.left,
e.clientY + boundingClientRect.top,
e.clientX + boundingClientRect.left,
e.clientY + boundingClientRect.top,
e.ctrlKey,
e.altKey,
e.shiftKey,
@ -84,48 +43,140 @@ function bubbleIframeMouseMove(iframe){
};
}
/**
* Default Etherpad frame width.
*/
const DEFAULT_WIDTH = 640;
/**
* Default Etherpad frame height.
*/
const DEFAULT_HEIGHT = 480;
var Etherpad = {
/**
* Initializes the etherpad.
*/
init: function (name) {
const EtherpadContainerType = "etherpad";
if (config.etherpad_base && !etherpadName && name) {
/**
* Container for Etherpad iframe.
*/
class Etherpad extends LargeContainer {
constructor (domain, name) {
super();
domain = config.etherpad_base;
const iframe = document.createElement('iframe');
etherpadName = name;
iframe.src = domain + name + '?' + options;
iframe.frameBorder = 0;
iframe.scrolling = "no";
iframe.width = DEFAULT_WIDTH;
iframe.height = DEFAULT_HEIGHT;
iframe.setAttribute('style', 'visibility: hidden;');
enableEtherpadButton();
this.container.appendChild(iframe);
/**
* Resizes the etherpad, when the window is resized.
*/
$(window).resize(function () {
resize();
});
}
},
iframe.onload = function() {
document.domain = document.domain;
bubbleIframeMouseMove(iframe);
/**
* Opens/hides the Etherpad.
*/
toggleEtherpad: function (isPresentation) {
if (!etherpadIFrame)
createIFrame();
setTimeout(function() {
const doc = iframe.contentDocument;
// the iframes inside of the etherpad are
// not yet loaded when the etherpad iframe is loaded
const outer = doc.getElementsByName("ace_outer")[0];
bubbleIframeMouseMove(outer);
if(VideoLayout.getLargeVideoState() === "etherpad")
{
VideoLayout.setLargeVideoState("video");
}
else
{
VideoLayout.setLargeVideoState("etherpad");
}
resize();
const inner = doc.getElementsByName("ace_inner")[0];
bubbleIframeMouseMove(inner);
}, 2000);
};
this.iframe = iframe;
}
};
module.exports = Etherpad;
get isOpen () {
return !!this.iframe;
}
get container () {
return document.getElementById('etherpad');
}
resize (containerWidth, containerHeight, animate) {
let height = containerHeight - BottomToolbar.getFilmStripHeight();
let width = containerWidth;
$(this.iframe).width(width).height(height);
}
show () {
const $iframe = $(this.iframe);
const $container = $(this.container);
return new Promise(resolve => {
$iframe.fadeIn(300, function () {
document.body.style.background = '#eeeeee';
$iframe.css({visibility: 'visible'});
$container.css({zIndex: 2});
resolve();
});
});
}
hide () {
const $iframe = $(this.iframe);
const $container = $(this.container);
return new Promise(resolve => {
$iframe.fadeOut(300, function () {
$iframe.css({visibility: 'hidden'});
$container.css({zIndex: 0});
resolve();
});
});
}
}
/**
* Manager of the Etherpad frame.
*/
export default class EtherpadManager {
constructor (domain, name) {
if (!domain || !name) {
throw new Error("missing domain or name");
}
this.domain = domain;
this.name = name;
this.etherpad = null;
}
get isOpen () {
return !!this.etherpad;
}
/**
* Create new Etherpad frame.
*/
openEtherpad () {
this.etherpad = new Etherpad(this.domain, this.name);
VideoLayout.addLargeVideoContainer(
EtherpadContainerType,
this.etherpad
);
}
/**
* Toggle Etherpad frame visibility.
* Open new Etherpad frame if there is no Etherpad frame yet.
*/
toggleEtherpad () {
if (!this.isOpen) {
this.openEtherpad();
}
let isVisible = VideoLayout.isLargeContainerTypeVisible(
EtherpadContainerType
);
VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible);
}
}

View File

@ -1,343 +1,448 @@
var ToolbarToggler = require("../toolbars/ToolbarToggler");
var UIUtil = require("../util/UIUtil");
var VideoLayout = require("../videolayout/VideoLayout");
var messageHandler = require("../util/MessageHandler");
var PreziPlayer = require("./PreziPlayer");
var preziPlayer = null;
/* global $, APP */
/* jshint -W101 */
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import PreziPlayer from './PreziPlayer';
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import messageHandler from '../util/MessageHandler';
import ToolbarToggler from "../toolbars/ToolbarToggler";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import BottomToolbar from '../toolbars/BottomToolbar';
/**
* Shows/hides a presentation.
* Example of Prezi link.
*/
function setPresentationVisible(visible) {
if (visible) {
VideoLayout.setLargeVideoState("prezi");
}
else {
VideoLayout.setLargeVideoState("video");
}
}
var Prezi = {
/**
* Reloads the current presentation.
*/
reloadPresentation: function() {
var iframe = document.getElementById(preziPlayer.options.preziId);
iframe.src = iframe.src;
},
/**
* Returns <tt>true</tt> if the presentation is visible, <tt>false</tt> -
* otherwise.
*/
isPresentationVisible: function () {
return ($('#presentation>iframe') != null
&& $('#presentation>iframe').css('opacity') == 1);
},
/**
* Opens the Prezi dialog, from which the user could choose a presentation
* to load.
*/
openPreziDialog: function() {
var myprezi = APP.xmpp.getPrezi();
if (myprezi) {
messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
null,
"dialog.removePreziMsg",
null,
false,
"dialog.Remove",
function(e,v,m,f) {
if(v) {
APP.xmpp.removePreziFromPresence();
}
}
);
}
else if (preziPlayer != null) {
messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
null, "dialog.sharePreziMsg",
null,
false,
"dialog.Ok",
function(e,v,m,f) {
$.prompt.close();
}
);
}
else {
var html = APP.translation.generateTranslationHTML(
"dialog.sharePreziTitle");
var cancelButton = APP.translation.generateTranslationHTML(
"dialog.Cancel");
var shareButton = APP.translation.generateTranslationHTML(
"dialog.Share");
var backButton = APP.translation.generateTranslationHTML(
"dialog.Back");
var buttons = [];
var buttons1 = [];
// Cancel button to both states
buttons.push({title: cancelButton, value: false});
buttons1.push({title: cancelButton, value: false});
// Share button
buttons.push({title: shareButton, value: true});
// Back button
buttons1.push({title: backButton, value: true});
var linkError = APP.translation.generateTranslationHTML(
"dialog.preziLinkError");
var defaultUrl = APP.translation.translateString("defaultPreziLink",
{url: "http://prezi.com/wz7vhjycl7e6/my-prezi"});
var openPreziState = {
state0: {
html: '<h2>' + html + '</h2>' +
'<input name="preziUrl" type="text" ' +
'data-i18n="[placeholder]defaultPreziLink" data-i18n-options=\'' +
JSON.stringify({"url": "http://prezi.com/wz7vhjycl7e6/my-prezi"}) +
'\' placeholder="' + defaultUrl + '" autofocus>',
persistent: false,
buttons: buttons,
focus: ':input:first',
defaultButton: 0,
submit: function (e, v, m, f) {
e.preventDefault();
if(v)
{
var preziUrl = f.preziUrl;
if (preziUrl)
{
var urlValue
= encodeURI(UIUtil.escapeHtml(preziUrl));
if (urlValue.indexOf('http://prezi.com/') != 0
&& urlValue.indexOf('https://prezi.com/') != 0)
{
$.prompt.goToState('state1');
return false;
}
else {
var presIdTmp = urlValue.substring(
urlValue.indexOf("prezi.com/") + 10);
if (!isAlphanumeric(presIdTmp)
|| presIdTmp.indexOf('/') < 2) {
$.prompt.goToState('state1');
return false;
}
else {
APP.xmpp.addToPresence("prezi", urlValue);
$.prompt.close();
}
}
}
}
else
$.prompt.close();
}
},
state1: {
html: '<h2>' + html + '</h2>' +
linkError,
persistent: false,
buttons: buttons1,
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
e.preventDefault();
if (v === 0)
$.prompt.close();
else
$.prompt.goToState('state0');
}
}
};
messageHandler.openDialogWithStates(openPreziState);
}
}
};
const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi";
const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i;
/**
* Default aspect ratio for Prezi frame.
*/
const aspectRatio = 16.0 / 9.0;
/**
* A new presentation has been added.
*
* @param event the event indicating the add of a presentation
* @param jid the jid from which the presentation was added
* @param presUrl url of the presentation
* @param currentSlide the current slide to which we should move
* Default Prezi frame width.
*/
function presentationAdded(event, jid, presUrl, currentSlide) {
console.log("presentation added", presUrl);
var presId = getPresentationId(presUrl);
var elementId = 'participant_'
+ Strophe.getResourceFromJid(jid)
+ '_' + presId;
VideoLayout.addPreziContainer(elementId);
var controlsEnabled = false;
if (jid === APP.xmpp.myJid())
controlsEnabled = true;
setPresentationVisible(true);
VideoLayout.setLargeVideoHover(
function (event) {
if (Prezi.isPresentationVisible()) {
var reloadButtonRight = window.innerWidth
- $('#presentation>iframe').offset().left
- $('#presentation>iframe').width();
$('#reloadPresentation').css({ right: reloadButtonRight,
display:'inline-block'});
}
},
function (event) {
if (!Prezi.isPresentationVisible())
$('#reloadPresentation').css({display:'none'});
else {
var e = event.toElement || event.relatedTarget;
if (e && e.id != 'reloadPresentation' && e.id != 'header')
$('#reloadPresentation').css({display:'none'});
}
});
preziPlayer = new PreziPlayer(
'presentation',
{preziId: presId,
width: getPresentationWidth(),
height: getPresentationHeihgt(),
controls: controlsEnabled,
debug: true
});
$('#presentation>iframe').attr('id', preziPlayer.options.preziId);
preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
console.log("prezi status", event.value);
if (event.value == PreziPlayer.STATUS_CONTENT_READY) {
if (jid != APP.xmpp.myJid())
preziPlayer.flyToStep(currentSlide);
}
});
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) {
console.log("event value", event.value);
APP.xmpp.addToPresence("preziSlide", event.value);
});
$("#" + elementId).css( 'background-image',
'url(../images/avatarprezi.png)');
$("#" + elementId).click(
function () {
setPresentationVisible(true);
}
);
};
const DEFAULT_WIDTH = 640;
/**
* A presentation has been removed.
*
* @param event the event indicating the remove of a presentation
* @param jid the jid for which the presentation was removed
* @param the url of the presentation
* Default Prezi frame height.
*/
function presentationRemoved(event, jid, presUrl) {
console.log('presentation removed', presUrl);
var presId = getPresentationId(presUrl);
setPresentationVisible(false);
$('#participant_'
+ Strophe.getResourceFromJid(jid)
+ '_' + presId).remove();
$('#presentation>iframe').remove();
if (preziPlayer != null) {
preziPlayer.destroy();
preziPlayer = null;
}
};
const DEFAULT_HEIGHT = 480;
/**
* Indicates if the given string is an alphanumeric string.
* Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
* purpose of checking URIs.
* @param {string} unsafeText string to check
* @returns {boolean}
*/
function isAlphanumeric(unsafeText) {
var regex = /^[a-z0-9-_\/&\?=;]+$/i;
return regex.test(unsafeText);
return alphanumRegex.test(unsafeText);
}
/**
* Returns the presentation id from the given url.
* @param {string} url Prezi link
* @returns {string} presentation id
*/
function getPresentationId (presUrl) {
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
return presIdTmp.substring(0, presIdTmp.indexOf('/'));
function getPresentationId (url) {
let presId = url.substring(url.indexOf("prezi.com/") + 10);
return presId.substring(0, presId.indexOf('/'));
}
/**
* Returns the presentation width.
* Checks if given string is Prezi url.
* @param {string} url string to check.
* @returns {boolean}
*/
function getPresentationWidth() {
var availableWidth = UIUtil.getAvailableVideoWidth();
var availableHeight = getPresentationHeihgt();
var aspectRatio = 16.0 / 9.0;
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
function isPreziLink(url) {
if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
return false;
}
return availableWidth;
}
/**
* Returns the presentation height.
*/
function getPresentationHeihgt() {
var remoteVideos = $('#remoteVideos');
return window.innerHeight - remoteVideos.outerHeight();
}
/**
* Resizes the presentation iframe.
*/
function resize() {
if ($('#presentation>iframe')) {
$('#presentation>iframe').width(getPresentationWidth());
$('#presentation>iframe').height(getPresentationHeihgt());
let presId = url.substring(url.indexOf("prezi.com/") + 10);
if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
return false;
}
return true;
}
/**
* Presentation has been removed.
* Notify user that other user if already sharing Prezi.
*/
$(document).bind('presentationremoved.muc', presentationRemoved);
function notifyOtherIsSharingPrezi() {
messageHandler.openMessageDialog(
"dialog.sharePreziTitle",
"dialog.sharePreziMsg"
);
}
/**
* Presentation has been added.
* Ask user if he want to close Prezi he's sharing.
*/
$(document).bind('presentationadded.muc', presentationAdded);
function proposeToClosePrezi() {
return new Promise(function (resolve, reject) {
messageHandler.openTwoButtonDialog(
"dialog.removePreziTitle",
null,
"dialog.removePreziMsg",
null,
false,
"dialog.Remove",
function(e,v,m,f) {
if (v) {
resolve();
} else {
reject();
}
}
);
/*
* Indicates presentation slide change.
});
}
/**
* Ask user for Prezi url to share with others.
* Dialog validates client input to allow only Prezi urls.
*/
$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
if (preziPlayer && preziPlayer.getCurrentStep() != current) {
preziPlayer.flyToStep(current);
function requestPreziLink() {
const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle");
const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
const shareButton = APP.translation.generateTranslationHTML("dialog.Share");
const backButton = APP.translation.generateTranslationHTML("dialog.Back");
const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError");
const i18nOptions = {url: defaultPreziLink};
const defaultUrl = APP.translation.translateString(
"defaultPreziLink", i18nOptions
);
var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
preziPlayer.flyToStep(current, i);
return new Promise(function (resolve, reject) {
let dialog = messageHandler.openDialogWithStates({
state0: {
html: `
<h2>${title}</h2>
<input name="preziUrl" type="text"
data-i18n="[placeholder]defaultPreziLink"
data-i18n-options="${JSON.stringify(i18nOptions)}"
placeholder="${defaultUrl}" autofocus>`,
persistent: false,
buttons: [
{title: cancelButton, value: false},
{title: shareButton, value: true}
],
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
e.preventDefault();
if (!v) {
reject('cancelled');
dialog.close();
return;
}
let preziUrl = f.preziUrl;
if (!preziUrl) {
return;
}
let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl));
if (!isPreziLink(urlValue)) {
dialog.goToState('state1');
return false;
}
resolve(urlValue);
dialog.close();
}
},
state1: {
html: `<h2>${title}</h2> ${linkError}`,
persistent: false,
buttons: [
{title: cancelButton, value: false},
{title: backButton, value: true}
],
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
e.preventDefault();
if (v === 0) {
reject();
dialog.close();
} else {
dialog.goToState('state0');
}
}
}
});
});
}
export const PreziContainerType = "prezi";
/**
* Container for Prezi iframe.
*/
class PreziContainer extends LargeContainer {
constructor ({preziId, isMy, slide, onSlideChanged}) {
super();
this.reloadBtn = $('#reloadPresentation');
let preziPlayer = new PreziPlayer(
'presentation', {
preziId,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
controls: isMy,
debug: true
}
);
this.preziPlayer = preziPlayer;
this.$iframe = $(preziPlayer.iframe);
this.$iframe.attr('id', preziId);
preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) {
console.log("prezi status", value);
if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) {
preziPlayer.flyToStep(slide);
}
});
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) {
console.log("event value", value);
onSlideChanged(value);
});
}
/**
* Change Prezi slide.
* @param {number} slide slide to show
*/
goToSlide (slide) {
if (this.preziPlayer.getCurrentStep() === slide) {
return;
}
this.preziPlayer.flyToStep(slide);
let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps();
if (!animationStepsArray) {
return;
}
for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) {
this.preziPlayer.flyToStep(slide, i);
}
}
});
$(window).resize(function () {
resize();
});
/**
* Show or hide "reload presentation" button.
* @param {boolean} show
*/
showReloadBtn (show) {
this.reloadBtn.css('display', show ? 'inline-block' : 'none');
}
module.exports = Prezi;
show () {
return new Promise(resolve => {
this.$iframe.fadeIn(300, () => {
this.$iframe.css({opacity: 1});
ToolbarToggler.dockToolbar(true);
resolve();
});
});
}
hide () {
return new Promise(resolve => {
this.$iframe.fadeOut(300, () => {
this.$iframe.css({opacity: 0});
this.showReloadBtn(false);
ToolbarToggler.dockToolbar(false);
resolve();
});
});
}
onHoverIn () {
let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width();
this.showReloadBtn(true);
this.reloadBtn.css('right', rightOffset);
}
onHoverOut (event) {
let e = event.toElement || event.relatedTarget;
if (e && e.id != 'reloadPresentation' && e.id != 'header') {
this.showReloadBtn(false);
}
}
resize (containerWidth, containerHeight) {
let height = containerHeight - BottomToolbar.getFilmStripHeight();
let width = containerWidth;
if (height < width / aspectRatio) {
width = Math.floor(height * aspectRatio);
}
this.$iframe.width(width).height(height);
}
/**
* Close Prezi frame.
*/
close () {
this.showReloadBtn(false);
this.preziPlayer.destroy();
this.$iframe.remove();
}
}
/**
* Manager of Prezi frames.
*/
export default class PreziManager {
constructor (emitter) {
this.emitter = emitter;
this.userId = null;
this.url = null;
this.prezi = null;
$("#reloadPresentationLink").click(this.reloadPresentation.bind(this));
}
get isPresenting () {
return !!this.userId;
}
get isMyPrezi () {
return this.userId === APP.conference.localId;
}
/**
* Check if user is currently sharing.
* @param {string} id user id to check for
*/
isSharing (id) {
return this.userId === id;
}
handlePreziButtonClicked () {
if (!this.isPresenting) {
requestPreziLink().then(
url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0),
err => console.error('PREZI CANCELED', err)
);
return;
}
if (this.isMyPrezi) {
proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI));
} else {
notifyOtherIsSharingPrezi();
}
}
/**
* Reload current Prezi frame.
*/
reloadPresentation () {
if (!this.prezi) {
return;
}
let iframe = this.prezi.$iframe[0];
iframe.src = iframe.src;
}
/**
* Show Prezi. Create new Prezi if there is no Prezi yet.
* @param {string} id owner id
* @param {string} url Prezi url
* @param {number} slide slide to show
*/
showPrezi (id, url, slide) {
if (!this.isPresenting) {
this.createPrezi(id, url, slide);
}
if (this.userId === id && this.url === url) {
this.prezi.goToSlide(slide);
} else {
console.error(this.userId, id);
console.error(this.url, url);
throw new Error("unexpected presentation change");
}
}
/**
* Create new Prezi frame..
* @param {string} id owner id
* @param {string} url Prezi url
* @param {number} slide slide to show
*/
createPrezi (id, url, slide) {
console.log("presentation added", url);
this.userId = id;
this.url = url;
let preziId = getPresentationId(url);
let elementId = `participant_${id}_${preziId}`;
this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId));
VideoLayout.resizeThumbnails();
this.$thumb.css({
'background-image': 'url(../images/avatarprezi.png)'
}).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true));
this.prezi = new PreziContainer({
preziId,
isMy: this.isMyPrezi,
slide,
onSlideChanged: newSlide => {
if (this.isMyPrezi) {
this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide);
}
}
});
VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi);
VideoLayout.showLargeVideoContainer(PreziContainerType, true);
}
/**
* Close Prezi.
* @param {string} id owner id
*/
removePrezi (id) {
if (this.userId !== id) {
throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`);
}
this.$thumb.remove();
this.$thumb = null;
// wait until Prezi is hidden, then remove it
VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => {
console.log("presentation removed", this.url);
VideoLayout.removeLargeVideoContainer(PreziContainerType);
this.userId = null;
this.url = null;
this.prezi.close();
this.prezi = null;
});
}
}

View File

@ -1,298 +1,290 @@
/* global PreziPlayer */
/* jshint -W101 */
(function() {
"use strict";
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
window.PreziPlayer = (function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
PreziPlayer.API_VERSION = 1;
PreziPlayer.CURRENT_STEP = 'currentStep';
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
PreziPlayer.CURRENT_OBJECT = 'currentObject';
PreziPlayer.STATUS_LOADING = 'loading';
PreziPlayer.STATUS_READY = 'ready';
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
PreziPlayer.EVENT_STATUS = "statusChange";
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
PreziPlayer.domain = "https://prezi.com";
PreziPlayer.path = "/player/";
PreziPlayer.players = {};
PreziPlayer.binded_methods = ['changesHandler'];
PreziPlayer.API_VERSION = 1;
PreziPlayer.CURRENT_STEP = 'currentStep';
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
PreziPlayer.CURRENT_OBJECT = 'currentObject';
PreziPlayer.STATUS_LOADING = 'loading';
PreziPlayer.STATUS_READY = 'ready';
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
PreziPlayer.EVENT_STATUS = "statusChange";
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
PreziPlayer.domain = "https://prezi.com";
PreziPlayer.path = "/player/";
PreziPlayer.players = {};
PreziPlayer.binded_methods = ['changesHandler'];
PreziPlayer.createMultiplePlayers = function(optionArray){
for(var i=0; i<optionArray.length; i++) {
var optionSet = optionArray[i];
new PreziPlayer(optionSet.id, optionSet);
PreziPlayer.createMultiplePlayers = function(optionArray){
for(var i=0; i<optionArray.length; i++) {
var optionSet = optionArray[i];
new PreziPlayer(optionSet.id, optionSet);
}
};
PreziPlayer.messageReceived = function(event){
var message, item, player;
try {
message = JSON.parse(event.data);
if (message.id && (player = PreziPlayer.players[message.id])) {
if (player.options.debug === true) {
if (console && console.log)
console.log('received', message);
}
};
PreziPlayer.messageReceived = function(event){
var message, item, player;
try {
message = JSON.parse(event.data);
if (message.id && (player = PreziPlayer.players[message.id])) {
if (player.options.debug === true) {
if (console && console.log)
console.log('received', message);
}
if (message.type === "changes") {
player.changesHandler(message);
}
for (var i = 0; i < player.callbacks.length; i++) {
item = player.callbacks[i];
if (item && message.type === item.event) {
item.callback(message);
}
}
if (message.type === "changes") {
player.changesHandler(message);
}
for (var i = 0; i < player.callbacks.length; i++) {
item = player.callbacks[i];
if (item && message.type === item.event) {
item.callback(message);
}
} catch (e) { }
};
}
}
} catch (e) { }
};
/*jshint -W004 */
function PreziPlayer(id, options) {
/*jshint +W004 */
var params, paramString = "", _this = this;
if (PreziPlayer.players[id]){
PreziPlayer.players[id].destroy();
}
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
var method_name = PreziPlayer.binded_methods[i];
_this[method_name] = __bind(_this[method_name], _this);
}
options = options || {};
this.options = options;
this.values = {'status': PreziPlayer.STATUS_LOADING};
this.values[PreziPlayer.CURRENT_STEP] = 0;
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
this.values[PreziPlayer.CURRENT_OBJECT] = null;
this.callbacks = [];
this.id = id;
this.embedTo = document.getElementById(id);
if (!this.embedTo) {
throw "The element id is not available.";
}
this.iframe = document.createElement('iframe');
params = [
{ name: 'oid', value: options.preziId },
{ name: 'explorable', value: options.explorable ? 1 : 0 },
{ name: 'controls', value: options.controls ? 1 : 0 }
];
for (i=0; i<params.length; i++) {
var param = params[i];
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
}
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
this.iframe.frameBorder = 0;
this.iframe.scrolling = "no";
this.iframe.width = options.width || 640;
this.iframe.height = options.height || 480;
this.embedTo.innerHTML = '';
// JITSI: IN CASE SOMETHING GOES WRONG.
try {
this.embedTo.appendChild(this.iframe);
}
catch (err) {
console.log("CATCH ERROR");
}
function PreziPlayer(id, options) {
/*jshint +W004 */
var params, paramString = "", _this = this;
if (PreziPlayer.players[id]){
PreziPlayer.players[id].destroy();
}
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
var method_name = PreziPlayer.binded_methods[i];
_this[method_name] = __bind(_this[method_name], _this);
}
options = options || {};
this.options = options;
this.values = {'status': PreziPlayer.STATUS_LOADING};
this.values[PreziPlayer.CURRENT_STEP] = 0;
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
this.values[PreziPlayer.CURRENT_OBJECT] = null;
this.callbacks = [];
this.id = id;
this.embedTo = document.getElementById(id);
if (!this.embedTo) {
throw "The element id is not available.";
}
this.iframe = document.createElement('iframe');
params = [
{ name: 'oid', value: options.preziId },
{ name: 'explorable', value: options.explorable ? 1 : 0 },
{ name: 'controls', value: options.controls ? 1 : 0 }
];
for (i=0; i<params.length; i++) {
var param = params[i];
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
}
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
this.iframe.frameBorder = 0;
this.iframe.scrolling = "no";
this.iframe.width = options.width || 640;
this.iframe.height = options.height || 480;
this.embedTo.innerHTML = '';
// JITSI: IN CASE SOMETHING GOES WRONG.
try {
this.embedTo.appendChild(this.iframe);
}
catch (err) {
console.log("CATCH ERROR");
}
// JITSI: Increase interval from 200 to 500, which fixes prezi
// crashes for us.
this.initPollInterval = setInterval(function(){
_this.sendMessage({'action': 'init'});
}, 500);
PreziPlayer.players[id] = this;
}
// JITSI: Increase interval from 200 to 500, which fixes prezi
// crashes for us.
this.initPollInterval = setInterval(function(){
_this.sendMessage({'action': 'init'});
}, 500);
PreziPlayer.players[id] = this;
}
PreziPlayer.prototype.changesHandler = function(message) {
var key, value, j, item;
if (this.initPollInterval) {
clearInterval(this.initPollInterval);
this.initPollInterval = false;
}
for (key in message.data) {
if (message.data.hasOwnProperty(key)){
value = message.data[key];
this.values[key] = value;
for (j=0; j<this.callbacks.length; j++) {
item = this.callbacks[j];
if (item && item.event === key + "Change"){
item.callback({type: item.event, value: value});
}
}
}
}
};
PreziPlayer.prototype.destroy = function() {
if (this.initPollInterval) {
clearInterval(this.initPollInterval);
this.initPollInterval = false;
}
this.embedTo.innerHTML = '';
};
PreziPlayer.prototype.sendMessage = function(message) {
if (this.options.debug === true) {
if (console && console.log) console.log('sent', message);
}
message.version = PreziPlayer.API_VERSION;
message.id = this.id;
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
};
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
PreziPlayer.prototype.flyToNextStep = function() {
return this.sendMessage({
'action': 'present',
'data': ['moveToNextStep']
});
};
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
PreziPlayer.prototype.flyToPreviousStep = function() {
return this.sendMessage({
'action': 'present',
'data': ['moveToPrevStep']
});
};
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
var obj = this;
// check animation_step
if (animation_step > 0 &&
obj.values.animationCountOnSteps &&
obj.values.animationCountOnSteps[step] <= animation_step) {
animation_step = obj.values.animationCountOnSteps[step];
}
// jump to animation steps by calling flyToNextStep()
function doAnimationSteps() {
if (obj.values.isMoving) {
setTimeout(doAnimationSteps, 100); // wait until the flight ends
return;
}
while (animation_step-- > 0) {
obj.flyToNextStep(); // do the animation steps
}
}
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
// jump to the step
return this.sendMessage({
'action': 'present',
'data': ['moveToStep', step]
});
};
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
PreziPlayer.prototype.flyToObject = function(objectId) {
return this.sendMessage({
'action': 'present',
'data': ['moveToObject', objectId]
});
};
PreziPlayer.prototype.play = function(defaultDelay) {
return this.sendMessage({
'action': 'present',
'data': ['startAutoPlay', defaultDelay]
});
};
PreziPlayer.prototype.stop = function() {
return this.sendMessage({
'action': 'present',
'data': ['stopAutoPlay']
});
};
PreziPlayer.prototype.pause = function(defaultDelay) {
return this.sendMessage({
'action': 'present',
'data': ['pauseAutoPlay', defaultDelay]
});
};
PreziPlayer.prototype.getCurrentStep = function() {
return this.values.currentStep;
};
PreziPlayer.prototype.getCurrentAnimationStep = function() {
return this.values.currentAnimationStep;
};
PreziPlayer.prototype.getCurrentObject = function() {
return this.values.currentObject;
};
PreziPlayer.prototype.getStatus = function() {
return this.values.status;
};
PreziPlayer.prototype.isPlaying = function() {
return this.values.isAutoPlaying;
};
PreziPlayer.prototype.getStepCount = function() {
return this.values.stepCount;
};
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
return this.values.animationCountOnSteps;
};
PreziPlayer.prototype.getTitle = function() {
return this.values.title;
};
PreziPlayer.prototype.setDimensions = function(dims) {
for (var parameter in dims) {
this.iframe[parameter] = dims[parameter];
}
};
PreziPlayer.prototype.getDimensions = function() {
return {
width: parseInt(this.iframe.width, 10),
height: parseInt(this.iframe.height, 10)
};
};
PreziPlayer.prototype.on = function(event, callback) {
this.callbacks.push({
event: event,
callback: callback
});
};
PreziPlayer.prototype.off = function(event, callback) {
var j, item;
if (event === undefined) {
this.callbacks = [];
}
j = this.callbacks.length;
while (j--) {
PreziPlayer.prototype.changesHandler = function(message) {
var key, value, j, item;
if (this.initPollInterval) {
clearInterval(this.initPollInterval);
this.initPollInterval = false;
}
for (key in message.data) {
if (message.data.hasOwnProperty(key)){
value = message.data[key];
this.values[key] = value;
for (j=0; j<this.callbacks.length; j++) {
item = this.callbacks[j];
if (item && item.event === event && (callback === undefined || item.callback === callback)){
this.callbacks.splice(j, 1);
if (item && item.event === key + "Change"){
item.callback({type: item.event, value: value});
}
}
};
if (window.addEventListener) {
window.addEventListener('message', PreziPlayer.messageReceived, false);
} else {
window.attachEvent('onmessage', PreziPlayer.messageReceived);
}
}
};
return PreziPlayer;
PreziPlayer.prototype.destroy = function() {
if (this.initPollInterval) {
clearInterval(this.initPollInterval);
this.initPollInterval = false;
}
this.embedTo.innerHTML = '';
};
})();
PreziPlayer.prototype.sendMessage = function(message) {
if (this.options.debug === true) {
if (console && console.log) console.log('sent', message);
}
message.version = PreziPlayer.API_VERSION;
message.id = this.id;
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
};
})();
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
PreziPlayer.prototype.flyToNextStep = function() {
return this.sendMessage({
'action': 'present',
'data': ['moveToNextStep']
});
};
module.exports = PreziPlayer;
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
PreziPlayer.prototype.flyToPreviousStep = function() {
return this.sendMessage({
'action': 'present',
'data': ['moveToPrevStep']
});
};
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
var obj = this;
// check animation_step
if (animation_step > 0 &&
obj.values.animationCountOnSteps &&
obj.values.animationCountOnSteps[step] <= animation_step) {
animation_step = obj.values.animationCountOnSteps[step];
}
// jump to animation steps by calling flyToNextStep()
function doAnimationSteps() {
if (obj.values.isMoving) {
setTimeout(doAnimationSteps, 100); // wait until the flight ends
return;
}
while (animation_step-- > 0) {
obj.flyToNextStep(); // do the animation steps
}
}
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
// jump to the step
return this.sendMessage({
'action': 'present',
'data': ['moveToStep', step]
});
};
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
PreziPlayer.prototype.flyToObject = function(objectId) {
return this.sendMessage({
'action': 'present',
'data': ['moveToObject', objectId]
});
};
PreziPlayer.prototype.play = function(defaultDelay) {
return this.sendMessage({
'action': 'present',
'data': ['startAutoPlay', defaultDelay]
});
};
PreziPlayer.prototype.stop = function() {
return this.sendMessage({
'action': 'present',
'data': ['stopAutoPlay']
});
};
PreziPlayer.prototype.pause = function(defaultDelay) {
return this.sendMessage({
'action': 'present',
'data': ['pauseAutoPlay', defaultDelay]
});
};
PreziPlayer.prototype.getCurrentStep = function() {
return this.values.currentStep;
};
PreziPlayer.prototype.getCurrentAnimationStep = function() {
return this.values.currentAnimationStep;
};
PreziPlayer.prototype.getCurrentObject = function() {
return this.values.currentObject;
};
PreziPlayer.prototype.getStatus = function() {
return this.values.status;
};
PreziPlayer.prototype.isPlaying = function() {
return this.values.isAutoPlaying;
};
PreziPlayer.prototype.getStepCount = function() {
return this.values.stepCount;
};
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
return this.values.animationCountOnSteps;
};
PreziPlayer.prototype.getTitle = function() {
return this.values.title;
};
PreziPlayer.prototype.setDimensions = function(dims) {
for (var parameter in dims) {
this.iframe[parameter] = dims[parameter];
}
};
PreziPlayer.prototype.getDimensions = function() {
return {
width: parseInt(this.iframe.width, 10),
height: parseInt(this.iframe.height, 10)
};
};
PreziPlayer.prototype.on = function(event, callback) {
this.callbacks.push({
event: event,
callback: callback
});
};
PreziPlayer.prototype.off = function(event, callback) {
var j, item;
if (event === undefined) {
this.callbacks = [];
}
j = this.callbacks.length;
while (j--) {
item = this.callbacks[j];
if (item && item.event === event && (callback === undefined || item.callback === callback)){
this.callbacks.splice(j, 1);
}
}
};
if (window.addEventListener) {
window.addEventListener('message', PreziPlayer.messageReceived, false);
} else {
window.attachEvent('onmessage', PreziPlayer.messageReceived);
}
window.PreziPlayer = PreziPlayer;
export default PreziPlayer;

View File

@ -1,102 +1,100 @@
/* global require, $ */
var Chat = require("./chat/Chat");
var ContactList = require("./contactlist/ContactList");
var Settings = require("./../../settings/Settings");
var SettingsMenu = require("./settings/SettingsMenu");
var VideoLayout = require("../videolayout/VideoLayout");
var ToolbarToggler = require("../toolbars/ToolbarToggler");
var UIUtil = require("../util/UIUtil");
var LargeVideo = require("../videolayout/LargeVideo");
import Chat from "./chat/Chat";
import ContactList from "./contactlist/ContactList";
import Settings from "./../../settings/Settings";
import SettingsMenu from "./settings/SettingsMenu";
import VideoLayout from "../videolayout/VideoLayout";
import ToolbarToggler from "../toolbars/ToolbarToggler";
import UIUtil from "../util/UIUtil";
const buttons = {
'#chatspace': '#chatBottomButton',
'#contactlist': '#contactListButton',
'#settingsmenu': '#toolbar_button_settings'
};
var currentlyOpen = null;
/**
* Toggles the windows in the side panel
* @param object the window that should be shown
* @param selector the selector for the element containing the panel
* @param onOpenComplete function to be called when the panel is opened
* @param onOpen function to be called if the window is going to be opened
* @param onClose function to be called if the window is going to be closed
*/
function toggle (object, selector, onOpenComplete, onOpen, onClose) {
UIUtil.buttonClick(buttons[selector], "active");
if (object.isVisible()) {
$("#toast-container").animate({
right: 5
}, {
queue: false,
duration: 500
});
$(selector).hide("slide", {
direction: "right",
queue: false,
duration: 500
});
if(typeof onClose === "function") {
onClose();
}
currentlyOpen = null;
} else {
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible()) {
ToolbarToggler.dockToolbar(false);
}
if (currentlyOpen) {
var current = $(currentlyOpen);
UIUtil.buttonClick(buttons[currentlyOpen], "active");
current.css('z-index', 4);
setTimeout(function () {
current.css('display', 'none');
current.css('z-index', 5);
}, 500);
}
$("#toast-container").animate({
right: (UIUtil.getSidePanelSize()[0] + 5)
}, {
queue: false,
duration: 500
});
$(selector).show("slide", {
direction: "right",
queue: false,
duration: 500,
complete: onOpenComplete
});
if(typeof onOpen === "function") {
onOpen();
}
currentlyOpen = selector;
}
}
/**
* Toggler for the chat, contact list, settings menu, etc..
*/
var PanelToggler = (function(my) {
var currentlyOpen = null;
var buttons = {
'#chatspace': '#chatBottomButton',
'#contactlist': '#contactListButton',
'#settingsmenu': '#toolbar_button_settings'
};
/**
* Toggles the windows in the side panel
* @param object the window that should be shown
* @param selector the selector for the element containing the panel
* @param onOpenComplete function to be called when the panel is opened
* @param onOpen function to be called if the window is going to be opened
* @param onClose function to be called if the window is going to be closed
*/
var toggle = function(object, selector, onOpenComplete, onOpen, onClose) {
UIUtil.buttonClick(buttons[selector], "active");
if (object.isVisible()) {
$("#toast-container").animate({
right: '5px'
},
{
queue: false,
duration: 500
});
$(selector).hide("slide", {
direction: "right",
queue: false,
duration: 500
});
if(typeof onClose === "function") {
onClose();
}
currentlyOpen = null;
}
else {
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (LargeVideo.isLargeVideoVisible()) {
ToolbarToggler.dockToolbar(false);
}
if(currentlyOpen) {
var current = $(currentlyOpen);
UIUtil.buttonClick(buttons[currentlyOpen], "active");
current.css('z-index', 4);
setTimeout(function () {
current.css('display', 'none');
current.css('z-index', 5);
}, 500);
}
$("#toast-container").animate({
right: (PanelToggler.getPanelSize()[0] + 5) + 'px'
},
{
queue: false,
duration: 500
});
$(selector).show("slide", {
direction: "right",
queue: false,
duration: 500,
complete: onOpenComplete
});
if(typeof onOpen === "function") {
onOpen();
}
currentlyOpen = selector;
}
};
var PanelToggler = {
/**
* Opens / closes the chat area.
*/
my.toggleChat = function() {
var chatCompleteFunction = Chat.isVisible() ?
function() {} : function () {
Chat.scrollChatToBottom();
$('#chatspace').trigger('shown');
};
toggleChat () {
var chatCompleteFunction = Chat.isVisible()
? function () {}
: function () {
Chat.scrollChatToBottom();
$('#chatspace').trigger('shown');
};
VideoLayout.resizeVideoArea(!Chat.isVisible(), chatCompleteFunction);
@ -112,16 +110,24 @@ var PanelToggler = (function(my) {
}
},
null,
Chat.resizeChat,
() => this.resizeChat(),
null);
};
},
resizeChat () {
let [width, height] = UIUtil.getSidePanelSize();
Chat.resizeChat(width, height);
},
/**
* Opens / closes the contact list area.
*/
my.toggleContactList = function () {
var completeFunction = ContactList.isVisible() ?
function() {} : function () { $('#contactlist').trigger('shown');};
toggleContactList () {
var completeFunction = ContactList.isVisible()
? function () {}
: function () {
$('#contactlist').trigger('shown');
};
VideoLayout.resizeVideoArea(!ContactList.isVisible(), completeFunction);
toggle(ContactList,
@ -131,12 +137,12 @@ var PanelToggler = (function(my) {
ContactList.setVisualNotification(false);
},
null);
};
},
/**
* Opens / closes the settings menu
*/
my.toggleSettingsMenu = function() {
toggleSettingsMenu () {
VideoLayout.resizeVideoArea(!SettingsMenu.isVisible(), function (){});
toggle(SettingsMenu,
'#settingsmenu',
@ -147,31 +153,13 @@ var PanelToggler = (function(my) {
$('#setEmail').get(0).value = settings.email;
},
null);
};
},
/**
* Returns the size of the side panel.
*/
my.getPanelSize = function () {
var availableHeight = window.innerHeight;
var availableWidth = window.innerWidth;
var panelWidth = 200;
if (availableWidth * 0.2 < 200) {
panelWidth = availableWidth * 0.2;
}
return [panelWidth, availableHeight];
};
my.isVisible = function() {
isVisible () {
return (Chat.isVisible() ||
ContactList.isVisible() ||
SettingsMenu.isVisible());
};
}
};
return my;
}(PanelToggler || {}));
module.exports = PanelToggler;
export default PanelToggler;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,66 +1,109 @@
/* global $ */
var PanelToggler = require("../side_pannels/SidePanelToggler");
var UIUtil = require("../util/UIUtil");
var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter");
var UIEvents = require("../../../service/UI/UIEvents");
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import AnalyticsAdapter from '../../statistics/AnalyticsAdapter';
var eventEmitter = null;
var buttonHandlers = {
"bottom_toolbar_contact_list": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.contacts.toggled');
BottomToolbar.toggleContactList();
},
"bottom_toolbar_film_strip": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.filmstrip.toggled');
BottomToolbar.toggleFilmStrip();
},
"bottom_toolbar_chat": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.chat.toggled');
BottomToolbar.toggleChat();
}
};
var defaultBottomToolbarButtons = {
'chat': '#bottom_toolbar_chat',
'contacts': '#bottom_toolbar_contact_list',
const defaultBottomToolbarButtons = {
'chat': '#bottom_toolbar_chat',
'contacts': '#bottom_toolbar_contact_list',
'filmstrip': '#bottom_toolbar_film_strip'
};
const BottomToolbar = {
init () {
this.filmStrip = $('#remoteVideos');
this.toolbar = $('#bottomToolbar');
},
var BottomToolbar = (function (my) {
my.init = function (emitter) {
eventEmitter = emitter;
setupListeners (emitter) {
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
for(var k in buttonHandlers)
$("#" + k).click(buttonHandlers[k]);
};
const buttonHandlers = {
"bottom_toolbar_contact_list": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.contacts.toggled');
emitter.emit(UIEvents.TOGGLE_CONTACT_LIST);
},
"bottom_toolbar_film_strip": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.filmstrip.toggled');
emitter.emit(UIEvents.TOGGLE_FILM_STRIP);
},
"bottom_toolbar_chat": function () {
AnalyticsAdapter.sendEvent('bottomtoolbar.chat.toggled');
emitter.emit(UIEvents.TOGGLE_CHAT);
}
};
my.toggleChat = function() {
PanelToggler.toggleChat();
};
Object.keys(buttonHandlers).forEach(
buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId])
);
},
my.toggleContactList = function() {
PanelToggler.toggleContactList();
};
toggleFilmStrip () {
this.filmStrip.toggleClass("hidden");
},
my.toggleFilmStrip = function() {
var filmstrip = $("#remoteVideos");
filmstrip.toggleClass("hidden");
isFilmStripVisible () {
return !this.filmStrip.hasClass('hidden');
},
eventEmitter.emit( UIEvents.FILM_STRIP_TOGGLED,
filmstrip.hasClass("hidden"));
};
setupFilmStripOnly () {
this.filmStrip.css({
padding: "0px 0px 18px 0px",
right: 0
});
},
$(document).bind("remotevideo.resized", function (event, width, height) {
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
getFilmStripHeight () {
if (this.isFilmStripVisible()) {
return this.filmStrip.outerHeight();
} else {
return 0;
}
},
$('#bottomToolbar').css({bottom: bottom + 'px'});
});
getFilmStripWidth () {
return this.filmStrip.width();
},
return my;
}(BottomToolbar || {}));
resizeThumbnails (thumbWidth, thumbHeight,
animate = false, forceUpdate = false) {
return new Promise(resolve => {
this.filmStrip.animate({
// adds 2 px because of small video 1px border
height: thumbHeight + 2
}, {
queue: false,
duration: animate ? 500 : 0
});
module.exports = BottomToolbar;
this.getThumbs(!forceUpdate).animate({
height: thumbHeight,
width: thumbWidth
}, {
queue: false,
duration: animate ? 500 : 0,
complete: resolve
});
if (!animate) {
resolve();
}
});
},
resizeToolbar (thumbWidth, thumbHeight) {
let bottom = (thumbHeight - this.toolbar.outerHeight())/2 + 18;
this.toolbar.css({bottom});
},
getThumbs (only_visible = false) {
let selector = 'span';
if (only_visible) {
selector += ':visible';
}
return this.filmStrip.children(selector);
}
};
export default BottomToolbar;

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

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
@ -417,13 +417,9 @@ var names = [
* Generate random username.
* @returns {string} random username
*/
function generateUsername () {
export function generateUsername () {
var name = RandomUtil.randomElement(names);
var suffix = RandomUtil.randomAlphanumStr(3);
return name + '-' + suffix;
}
module.exports = {
generateUsername: generateUsername
};

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

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

View File

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

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',
PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login',
FOCUS_DISCONNECTED: 'xmpp.focus_disconnected',
ROOM_JOIN_ERROR: 'xmpp.room_join_error',
ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
// xmpp is connected and obtained user media
READY_TO_JOIN: 'xmpp.ready_to_join'
};