/* 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 localStream is video one this method will advertise it's * video type in MUC presence. * @param localStream new or modified LocalStream. */ 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: "" + "" + "", 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; var password = null; if (config.token) { password = config.token; if (config.id) { jid = config.id + "@" + jid; } else { jid = generateUserName() + "@" + jid; } } connect(jid, password); }, createConnection: function () { var bosh = config.bosh || '/http-bind'; // adds the room name used to the bosh connection return new Strophe.Connection(bosh + '?room=' + APP.UI.getRoomNode()); }, 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 null 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;