(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.APP = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.listenerCount = function(emitter, type) { var ret; if (!emitter._events || !emitter._events[type]) ret = 0; else if (isFunction(emitter._events[type])) ret = 1; else ret = emitter._events[type].length; return ret; }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } },{}],2:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; var queue = []; var draining = false; function drainQueue() { if (draining) { return; } draining = true; var currentQueue; var len = queue.length; while(len) { currentQueue = queue; queue = []; var i = -1; while (++i < len) { currentQueue[i](); } len = queue.length; } draining = false; } process.nextTick = function (fun) { queue.push(fun); if (!draining) { setTimeout(drainQueue, 0); } }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.binding = function (name) { throw new Error('process.binding is not supported'); }; // TODO(shtylman) process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; },{}],3:[function(require,module,exports){ /* jshint -W117 */ /* application specific logic */ var APP = { init: function () { this.UI = require("./modules/UI/UI"); this.API = require("./modules/API/API"); 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"); } }; function init() { APP.RTC.start(); APP.xmpp.start(); APP.statistics.start(); APP.connectionquality.init(); // Set default desktop sharing method APP.desktopsharing.init(); APP.keyboardshortcut.init(); APP.members.start(); } $(document).ready(function () { var URLPRocessor = require("./modules/URLProcessor/URLProcessor"); URLPRocessor.setConfigParametersFromUrl(); APP.init(); APP.translation.init(); if(APP.API.isEnabled()) APP.API.init(); APP.UI.start(init); }); $(window).bind('beforeunload', function () { if(APP.API.isEnabled()) APP.API.dispose(); }); module.exports = APP; },{"./modules/API/API":4,"./modules/DTMF/DTMF":5,"./modules/RTC/RTC":9,"./modules/UI/UI":11,"./modules/URLProcessor/URLProcessor":38,"./modules/connectionquality/connectionquality":39,"./modules/desktopsharing/desktopsharing":40,"./modules/keyboardshortcut/keyboardshortcut":41,"./modules/members/MemberList":42,"./modules/settings/Settings":43,"./modules/statistics/statistics":46,"./modules/translation/translation":47,"./modules/xmpp/xmpp":61}],4:[function(require,module,exports){ /** * Implements API class that communicates with external api class * and provides interface to access Jitsi Meet features by external * applications that embed Jitsi Meet */ var XMPPEvents = require("../../service/xmpp/XMPPEvents"); /** * List of the available commands. * @type {{ * displayName: inputDisplayNameHandler, * muteAudio: toggleAudio, * muteVideo: toggleVideo, * filmStrip: toggleFilmStrip * }} */ var commands = { displayName: APP.UI.inputDisplayNameHandler, muteAudio: APP.UI.toggleAudio, muteVideo: APP.UI.toggleVideo, toggleFilmStrip: APP.UI.toggleFilmStrip, toggleChat: APP.UI.toggleChat, toggleContactList: APP.UI.toggleContactList }; /** * Maps the supported events and their status * (true it the event is enabled and false if it is disabled) * @type {{ * incomingMessage: boolean, * outgoingMessage: boolean, * displayNameChange: boolean, * participantJoined: boolean, * participantLeft: boolean * }} */ var events = { incomingMessage: false, outgoingMessage:false, displayNameChange: false, participantJoined: false, participantLeft: false }; var displayName = {}; /** * Processes commands from external applicaiton. * @param message the object with the command */ function processCommand(message) { if(message.action != "execute") { console.error("Unknown action of the message"); return; } for(var key in message) { if(commands[key]) commands[key].apply(null, message[key]); } } /** * Processes events objects from external applications * @param event the event */ function processEvent(event) { if(!event.action) { console.error("Event with no action is received."); return; } var i = 0; switch(event.action) { case "add": for(; i < event.events.length; i++) { events[event.events[i]] = true; } break; case "remove": for(; i < event.events.length; i++) { events[event.events[i]] = false; } break; default: console.error("Unknown action for event."); } } /** * Sends message to the external application. * @param object */ function sendMessage(object) { window.parent.postMessage(JSON.stringify(object), "*"); } /** * Processes a message event from the external application * @param event the message event */ function processMessage(event) { var message; try { message = JSON.parse(event.data); } catch (e) {} if(!message.type) return; switch (message.type) { case "command": processCommand(message); break; case "event": processEvent(message); break; default: console.error("Unknown type of the message"); return; } } 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) { if (from != myjid) API.triggerEvent("incomingMessage", {"from": from, "nick": nick, "message": txt}); }); APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, function (jid) { API.triggerEvent("participantLeft", {jid: jid}); }); APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, function (jid, newDisplayName) { name = displayName[jid]; if(!name || name != newDisplayName) { API.triggerEvent("displayNameChange", {jid: jid, displayname: newDisplayName}); displayName[jid] = newDisplayName; } }); APP.xmpp.addListener(XMPPEvents.SENDING_CHAT_MESSAGE, function (body) { APP.API.triggerEvent("outgoingMessage", {"message": body}); }); } var API = { /** * Check whether the API should be enabled or not. * @returns {boolean} */ isEnabled: function () { var hash = location.hash; if(hash && hash.indexOf("external") > -1 && window.postMessage) return true; return false; }, /** * Initializes the APIConnector. Setups message event listeners that will * receive information from external applications that embed Jitsi Meet. * It also sends a message to the external application that APIConnector * is initialized. */ init: function () { if (window.addEventListener) { 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 */ triggerEvent: function (name, object) { if(this.isEnabled() && this.isEventEnabled(name)) sendMessage({ type: "event", action: "result", event: name, result: object}); }, /** * Removes the listeners. */ dispose: function () { if(window.removeEventListener) { window.removeEventListener("message", processMessage, false); } else { window.detachEvent('onmessage', processMessage); } } }; module.exports = API; },{"../../service/xmpp/XMPPEvents":108}],5:[function(require,module,exports){ /* 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; },{}],6:[function(require,module,exports){ /* global Strophe, focusedVideoSrc*/ // 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)); // when the data channel becomes available, tell the bridge about video // selections so that it can do adaptive simulcast, // we want the notification to trigger even if userJid is undefined, // or null. var userJid = APP.UI.getLargeVideoState().userResourceJid; // we want the notification to trigger even if userJid is undefined, // or null. onSelectedEndpointChanged(userJid); }; 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 = new Boolean(oldValue).valueOf(); } } if ((type = typeof newValue) !== 'boolean') { if (type === 'string') { newValue = (newValue == "true"); } else { newValue = new 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; var stream = obj.stream; console.log( "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); } } }; 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: onSelectedEndpointChanged, handlePinnedEndpointEvent: onPinnedEndpointChanged }; function onSelectedEndpointChanged(userResource) { console.log('selected endpoint changed: ', userResource); if (_dataChannels && _dataChannels.length != 0) { _dataChannels.some(function (dataChannel) { if (dataChannel.readyState == 'open') { console.log('sending selected endpoint changed ' + 'notification to the bridge: ', userResource); dataChannel.send(JSON.stringify({ 'colibriClass': 'SelectedEndpointChangedEvent', 'selectedEndpoint': (!userResource || userResource === null)? null : userResource })); return true; } }); } } function onPinnedEndpointChanged(userResource) { console.log('pinned endpoint changed: ', userResource); if (_dataChannels && _dataChannels.length != 0) { _dataChannels.some(function (dataChannel) { if (dataChannel.readyState == 'open') { dataChannel.send(JSON.stringify({ 'colibriClass': 'PinnedEndpointChangedEvent', 'pinnedEndpoint': (!userResource || userResource == null)? null : userResource })); return true; } }); } } module.exports = DataChannels; },{"../../service/RTC/RTCEvents":99}],7:[function(require,module,exports){ var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js"); 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(type == "audio") { this.getTracks = function () { return self.stream.getAudioTracks(); }; } else { this.getTracks = function () { return self.stream.getVideoTracks(); }; } this.stream.onended = function() { self.streamEnded(); }; } 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 this.type === "audio"; }; LocalStream.prototype.setMute = function(mute) { if((window.location.protocol != "https:" && this.isGUMStream) || (this.isAudioStream() && this.isGUMStream) || this.videoType === "screen") { var tracks = this.getTracks(); for (var idx = 0; idx < tracks.length; idx++) { tracks[idx].enabled = mute; } } else { if(mute === false) { APP.xmpp.removeStream(this.stream); this.stream.stop(); } else { var self = this; APP.RTC.rtcUtils.obtainAudioAndVideoPermissions( (this.isAudioStream() ? ["audio"] : ["video"]), function (stream) { if(self.isAudioStream()) { APP.RTC.changeLocalAudio(stream, function () {}); } else { APP.RTC.changeLocalVideo(stream, false, function () {}); } }); } } }; LocalStream.prototype.isMuted = function () { var tracks = []; if(this.type == "audio") { tracks = this.stream.getAudioTracks(); } else { if(this.stream.ended) 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; } module.exports = LocalStream; },{"../../service/RTC/StreamEventTypes.js":101}],8:[function(require,module,exports){ ////These lines should be uncommented when require works in app.js var MediaStreamType = require("../../service/RTC/MediaStreamTypes"); var StreamEventType = require("../../service/RTC/StreamEventTypes"); /** * 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 sid the session id * @param ssrc the ssrc corresponding to this MediaStream * * @constructor */ function MediaStream(data, sid, ssrc, browser, eventEmitter) { // 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. this.sid = sid; this.stream = data.stream; this.peerjid = data.peerjid; this.ssrc = ssrc; this.type = (this.stream.getVideoTracks().length > 0)? MediaStreamType.VIDEO_TYPE : MediaStreamType.AUDIO_TYPE; this.videoType = null; this.muted = false; this.eventEmitter = eventEmitter; } MediaStream.prototype.getOriginalStream = function() { return this.stream; }; MediaStream.prototype.setMute = function (value) { this.stream.muted = value; this.muted = value; }; MediaStream.prototype.setVideoType = function (value) { if(this.videoType === value) return; this.videoType = value; this.eventEmitter.emit(StreamEventType.EVENT_TYPE_REMOTE_CHANGED, this.peerjid); }; module.exports = MediaStream; },{"../../service/RTC/MediaStreamTypes":97,"../../service/RTC/StreamEventTypes":101}],9:[function(require,module,exports){ var EventEmitter = require("events"); 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 = { rtcUtils: null, devices: { audio: true, video: true }, localStreams: [], 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); //in firefox we have only one stream object if(this.localStreams.length == 0 || this.localStreams[0].getOriginalStream() != stream) this.localStreams.push(localStream); if(isMuted === true) localStream.setMute(false); if(type == "audio") { 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; }, removeLocalStream: function (stream) { for(var i = 0; i < this.localStreams.length; i++) { if(this.localStreams[i].getOriginalStream() === stream) { delete this.localStreams[i]; return; } } }, createRemoteStream: function (data, sid, thessrc) { var remoteStream = new MediaStream(data, sid, thessrc, this.getBrowserType(), eventEmitter); var jid = data.peerjid || APP.xmpp.myJid(); if(!this.remoteStreams[jid]) { this.remoteStreams[jid] = {}; } this.remoteStreams[jid][remoteStream.type]= remoteStream; eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, remoteStream); return remoteStream; }, getBrowserType: function () { return this.rtcUtils.browser; }, 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 (element, stream) { this.rtcUtils.attachMediaStream(element, 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); }, dispose: function() { if (this.rtcUtils) { this.rtcUtils = null; } }, stop: function () { this.dispose(); }, start: function () { var self = this; APP.desktopsharing.addListener( function (stream, isUsingScreenStream, callback) { self.changeLocalVideo(stream, isUsingScreenStream, callback); }, DesktopSharingEventTypes.NEW_STREAM_CREATED); APP.xmpp.addListener(XMPPEvents.STREAMS_CHANGED, function (jid, changedStreams) { for(var i = 0; i < changedStreams.length; i++) { var type = changedStreams[i].type; if (type != "audio") { var peerStreams = self.remoteStreams[jid]; if(!peerStreams) continue; var videoStream = peerStreams[MediaStreamType.VIDEO_TYPE]; if(!videoStream) continue; videoStream.setVideoType(changedStreams[i].type); } } }); 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); this.rtcUtils = new RTCUtils(this); this.rtcUtils.obtainAudioAndVideoPermissions( null, null, getMediaStreamUsage()); }, 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; }, switchVideoStreams: function (new_stream) { this.localVideo.stream = new_stream; this.localStreams = []; //in firefox we have only one stream object if (this.localAudio.getOriginalStream() != new_stream) this.localStreams.push(this.localAudio); this.localStreams.push(this.localVideo); }, changeLocalVideo: function (stream, isUsingScreenStream, callback) { var oldStream = this.localVideo.getOriginalStream(); var type = (isUsingScreenStream? "screen" : "video"); var localCallback = callback; if(this.localVideo.isMuted() && this.localVideo.videoType !== type) { localCallback = function() { APP.xmpp.setVideoMute(false, APP.UI.setVideoMuteButtonsState); callback(); }; } var videoStream = this.rtcUtils.createStream(stream, true); this.localVideo = this.createLocalStream(videoStream, "video", true, type); // Stop the stream to trigger onended event for old stream oldStream.stop(); 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, "audio", true); // Stop the stream to trigger onended event for old stream oldStream.stop(); APP.xmpp.switchStreams(newStream, oldStream, callback, true); }, /** * Checks if video identified by given src is desktop stream. * @param videoSrc eg. * blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395 * @returns {boolean} */ isVideoSrcDesktop: function (jid) { if(!jid) return false; var isDesktop = false; var stream = null; if (APP.xmpp.myJid() === jid) { // local video stream = this.localVideo; } else { var peerStreams = this.remoteStreams[jid]; if(!peerStreams) return false; stream = peerStreams[MediaStreamType.VIDEO_TYPE]; } if(stream) isDesktop = (stream.videoType === "screen"); return isDesktop; }, 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); } }; module.exports = RTC; },{"../../service/RTC/MediaStreamTypes":97,"../../service/RTC/RTCEvents.js":99,"../../service/RTC/StreamEventTypes.js":101,"../../service/UI/UIEvents":102,"../../service/desktopsharing/DesktopSharingEventTypes":105,"../../service/xmpp/XMPPEvents":108,"./DataChannels":6,"./LocalStream.js":7,"./MediaStream.js":8,"./RTCUtils.js":10,"events":1}],10:[function(require,module,exports){ var RTCBrowserType = require("../../service/RTC/RTCBrowserType.js"); var Resolutions = require("../../service/RTC/Resolutions"); 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 == null || (res.order < tmp.order && tmp.order < order)) { resName = i; res = tmp; } } return resName; } function setResolutionConstraints(constraints, resolution, isAndroid) { if (resolution && !constraints.video || isAndroid) { constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } if(Resolutions[resolution]) { constraints.video.mandatory.minWidth = Resolutions[resolution].width; constraints.video.mandatory.minHeight = Resolutions[resolution].height; } else { if (isAndroid) { 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, isAndroid) { var constraints = {audio: false, video: false}; if (um.indexOf('video') >= 0) { constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } if (um.indexOf('audio') >= 0) { constraints.audio = { mandatory: {}, optional: []};// same behaviour as true } if (um.indexOf('screen') >= 0) { constraints.video = { mandatory: { chromeMediaSource: "screen", googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 }, optional: [] }; } 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 (constraints.audio) { // 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} ); } if (constraints.video) { constraints.video.optional.push( {googNoiseReduction: false} // chrome 37 workaround for issue 3807, reenable in M38 ); if (um.indexOf('video') >= 0) { constraints.video.optional.push( {googLeakyBucket: true} ); } } if (um.indexOf('video') >= 0) { setResolutionConstraints(constraints, resolution, isAndroid); } if (bandwidth) { // doesn't work currently, see webrtc issue 1846 if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true 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) constraints.video = {mandatory: {}, optional: []};// same behaviour as true; constraints.video.mandatory.minFrameRate = fps; } return constraints; } function RTCUtils(RTCService) { this.service = RTCService; if (navigator.mozGetUserMedia) { console.log('This appears to be Firefox'); var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); if (version >= 40 && config.useBundle && config.useRtcpMux) { this.peerconnection = mozRTCPeerConnection; this.browser = RTCBrowserType.RTC_BROWSER_FIREFOX; 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 tracks = stream.getVideoTracks(); if(!tracks || tracks.length == 0) { tracks = stream.getAudioTracks(); } return tracks[0].id.replace(/[\{,\}]/g,""); }; this.getVideoSrc = function (element) { if(!element) return null; return element.mozSrcObject; }; this.setVideoSrc = function (element, src) { if(element) element.mozSrcObject = src; }; RTCSessionDescription = mozRTCSessionDescription; RTCIceCandidate = mozRTCIceCandidate; } else { window.location.href = 'unsupported_browser.html'; return; } } else if (navigator.webkitGetUserMedia) { console.log('This appears to be Chrome'); this.peerconnection = webkitRTCPeerConnection; this.browser = RTCBrowserType.RTC_BROWSER_CHROME; 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 stream.id.replace(/[\{,\}]/g,""); }; 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 (navigator.userAgent.indexOf('Android') != -1) { 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; }; } } else { try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { } window.location.href = 'unsupported_browser.html'; return; } } RTCUtils.prototype.getUserMediaWithConstraints = function( um, success_callback, failure_callback, resolution,bandwidth, fps, desktopStream) { currentResolution = resolution; // Check if we are running on Android device var isAndroid = navigator.userAgent.indexOf('Android') != -1; var constraints = getConstraints( um, resolution, bandwidth, fps, desktopStream, isAndroid); var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 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); if (failure_callback) { failure_callback(error); } }); } catch (e) { console.error('GUM 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 (navigator.mozGetUserMedia) { // With FF 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. self.getUserMediaWithConstraints( ['audio'], function (audioStream) { self.getUserMediaWithConstraints( ['video'], function (videoStream) { return self.successCallback({ audioStream: audioStream, videoStream: videoStream }); }, function (error) { console.error('failed to obtain video stream - stop', error); return self.successCallback(null); }, config.resolution || '360'); }, function (error) { console.error('failed to obtain audio stream - stop', error); return self.successCallback(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, the stream parameter is *not* a MediaStream object, it's // an object with two properties: audioStream, videoStream. if(stream && !navigator.mozGetUserMedia) 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 != null) { 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 {//firefox audioStream = stream.audioStream; videoStream = stream.videoStream; } 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, "audio", null, null, audioMuted, audioGUM); this.service.createLocalStream(videoStream, "video", null, null, videoMuted, videoGUM); }; 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 (i = 0; i < tracks.length; i++) { newStream.addTrack(tracks[i]); } } } else newStream = stream; return newStream; }; module.exports = RTCUtils; },{"../../service/RTC/RTCBrowserType.js":98,"../../service/RTC/Resolutions":100}],11:[function(require,module,exports){ var UI = {}; var VideoLayout = require("./videolayout/VideoLayout.js"); var AudioLevels = require("./audio_levels/AudioLevels.js"); var Prezi = require("./prezi/Prezi.js"); var Etherpad = require("./etherpad/Etherpad.js"); var Chat = require("./side_pannels/chat/Chat.js"); var Toolbar = require("./toolbars/Toolbar"); var ToolbarToggler = require("./toolbars/ToolbarToggler"); var BottomToolbar = require("./toolbars/BottomToolbar"); var ContactList = require("./side_pannels/contactlist/ContactList"); var Avatar = require("./avatar/Avatar"); var EventEmitter = require("events"); var SettingsMenu = require("./side_pannels/settings/SettingsMenu"); var Settings = require("./../settings/Settings"); var PanelToggler = require("./side_pannels/SidePanelToggler"); var RoomNameGenerator = require("./welcome_page/RoomnameGenerator"); UI.messageHandler = require("./util/MessageHandler"); var messageHandler = UI.messageHandler; var Authentication = require("./authentication/Authentication"); var UIUtil = require("./util/UIUtil"); var NicknameHandler = require("./util/NicknameHandler"); var CQEvents = require("../../service/connectionquality/CQEvents"); var DesktopSharingEventTypes = require("../../service/desktopsharing/DesktopSharingEventTypes"); var RTCEvents = require("../../service/RTC/RTCEvents"); var StreamEventTypes = require("../../service/RTC/StreamEventTypes"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var MemberEvents = require("../../service/members/Events"); var eventEmitter = new EventEmitter(); var roomName = null; function notifyForInitialMute() { messageHandler.notify(null, "notify.mutedTitle", "connected", "notify.muted", null, {timeOut: 120000}); } function setupPrezi() { $("#reloadPresentationLink").click(function() { Prezi.reloadPresentation(); }); } function setupChat() { Chat.init(); $("#toggle_smileys").click(function() { Chat.toggleSmileys(); }); } function setupToolbars() { Toolbar.init(UI); Toolbar.setupButtonsFromConfig(); BottomToolbar.init(); } function streamHandler(stream, isMuted) { switch (stream.type) { case "audio": VideoLayout.changeLocalAudio(stream, isMuted); break; case "video": VideoLayout.changeLocalVideo(stream, isMuted); break; case "stream": VideoLayout.changeLocalStream(stream, isMuted); break; } } function onXmppConnectionFailed(stropheErrorMsg) { var title = APP.translation.generateTranslatonHTML( "dialog.error"); var message; if (stropheErrorMsg) { message = APP.translation.generateTranslatonHTML( "dialog.connectErrorWithMsg", {msg: stropheErrorMsg}); } else { message = APP.translation.generateTranslatonHTML( "dialog.connectError"); } messageHandler.openDialog( title, message, true, {}, function (e, v, m, f) { return false; }); } function onDisposeConference(unload) { Toolbar.showAuthenticateButton(false); } function onDisplayNameChanged(jid, displayName) { ContactList.onDisplayNameChange(jid, displayName); SettingsMenu.onDisplayNameChange(jid, displayName); VideoLayout.onDisplayNameChanged(jid, displayName); } function registerListeners() { APP.RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CREATED); APP.RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED); APP.RTC.addStreamListener(function (stream) { VideoLayout.onRemoteStreamAdded(stream); }, StreamEventTypes.EVENT_TYPE_REMOTE_CREATED); APP.RTC.addStreamListener(function (jid) { VideoLayout.onVideoTypeChanged(jid); }, StreamEventTypes.EVENT_TYPE_REMOTE_CHANGED); APP.RTC.addListener(RTCEvents.LASTN_CHANGED, onLastNChanged); APP.RTC.addListener(RTCEvents.DOMINANTSPEAKER_CHANGED, function (resourceJid) { VideoLayout.onDominantSpeakerChanged(resourceJid); }); APP.RTC.addListener(RTCEvents.LASTN_ENDPOINT_CHANGED, function (lastNEndpoints, endpointsEnteringLastN, stream) { VideoLayout.onLastNEndpointsChanged(lastNEndpoints, endpointsEnteringLastN, stream); }); APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) { VideoLayout.setDeviceAvailabilityIcons(null, devices); }) APP.statistics.addAudioLevelListener(function(jid, audioLevel) { var resourceJid; if(jid === APP.statistics.LOCAL_JID) { resourceJid = AudioLevels.LOCAL_LEVEL; if(APP.RTC.localAudio.isMuted()) { audioLevel = 0; } } else { resourceJid = Strophe.getResourceFromJid(jid); } AudioLevels.updateAudioLevel(resourceJid, audioLevel, UI.getLargeVideoState().userResourceJid); }); APP.desktopsharing.addListener(function () { ToolbarToggler.showDesktopSharingButton(); }, DesktopSharingEventTypes.INIT); APP.desktopsharing.addListener( Toolbar.changeDesktopSharingButtonState, DesktopSharingEventTypes.SWITCHING_DONE); APP.connectionquality.addListener(CQEvents.LOCALSTATS_UPDATED, VideoLayout.updateLocalConnectionStats); APP.connectionquality.addListener(CQEvents.REMOTESTATS_UPDATED, VideoLayout.updateConnectionStats); APP.connectionquality.addListener(CQEvents.STOP, VideoLayout.onStatsStop); APP.xmpp.addListener(XMPPEvents.CONNECTION_FAILED, onXmppConnectionFailed); APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference); APP.xmpp.addListener(XMPPEvents.GRACEFUL_SHUTDOWN, function () { messageHandler.openMessageDialog( 'dialog.serviceUnavailable', 'dialog.gracefulShutdown' ); }); APP.xmpp.addListener(XMPPEvents.RESERVATION_ERROR, function (code, msg) { var title = APP.translation.generateTranslatonHTML( "dialog.reservationError"); var message = APP.translation.generateTranslatonHTML( "dialog.reservationErrorMsg", {code: code, msg: msg}); messageHandler.openDialog( title, message, true, {}, function (event, value, message, formVals) { return false; } ); }); APP.xmpp.addListener(XMPPEvents.KICKED, function () { messageHandler.openMessageDialog("dialog.sessTerminated", "dialog.kickMessage"); }); APP.xmpp.addListener(XMPPEvents.MUC_DESTROYED, function (reason) { //FIXME: use Session Terminated from translation, but // 'reason' text comes from XMPP packet and is not translated var title = APP.translation.generateTranslatonHTML("dialog.sessTerminated"); messageHandler.openDialog( title, reason, true, {}, function (event, value, message, formVals) { return false; } ); }); APP.xmpp.addListener(XMPPEvents.BRIDGE_DOWN, function () { messageHandler.showError("dialog.error", "dialog.bridgeUnavailable"); }); APP.xmpp.addListener(XMPPEvents.USER_ID_CHANGED, function (from, id) { Avatar.setUserAvatar(from, id); }); APP.xmpp.addListener(XMPPEvents.STREAMS_CHANGED, function (jid, changedStreams) { for(stream in changedStreams) { // might need to update the direction if participant just went from sendrecv to recvonly if (stream.type === 'video' || stream.type === 'screen') { var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video'); switch (stream.direction) { case 'sendrecv': el.show(); break; case 'recvonly': el.hide(); // FIXME: Check if we have to change large video //VideoLayout.updateLargeVideo(el); break; } } } }); APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, onDisplayNameChanged); APP.xmpp.addListener(XMPPEvents.MUC_JOINED, onMucJoined); APP.xmpp.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, onLocalRoleChanged); APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_JOINED, onMucMemberJoined); APP.xmpp.addListener(XMPPEvents.MUC_ROLE_CHANGED, onMucRoleChanged); APP.xmpp.addListener(XMPPEvents.PRESENCE_STATUS, onMucPresenceStatus); APP.xmpp.addListener(XMPPEvents.SUBJECT_CHANGED, chatSetSubject); APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, updateChatConversation); APP.xmpp.addListener(XMPPEvents.MUC_MEMBER_LEFT, onMucMemberLeft); APP.xmpp.addListener(XMPPEvents.PASSWORD_REQUIRED, onPasswordRequired); APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError); APP.xmpp.addListener(XMPPEvents.ETHERPAD, initEtherpad); APP.xmpp.addListener(XMPPEvents.AUTHENTICATION_REQUIRED, onAuthenticationRequired); APP.xmpp.addListener(XMPPEvents.DEVICE_AVAILABLE, function (resource, devices) { VideoLayout.setDeviceAvailabilityIcons(resource, devices); }); APP.members.addListener(MemberEvents.DTMF_SUPPORT_CHANGED, onDtmfSupportChanged); APP.xmpp.addListener(XMPPEvents.START_MUTED, function (audio, video) { SettingsMenu.setStartMuted(audio, video); }); } /** * Mutes/unmutes the local video. * * @param mute true to mute the local video; otherwise, false * @param options an object which specifies optional arguments such as the * boolean key byUser with default value true which * specifies whether the method was initiated in response to a user command (in * contrast to an automatic decision taken by the application logic) */ function setVideoMute(mute, options) { APP.RTC.setVideoMute(mute, UI.setVideoMuteButtonsState, options); } function bindEvents() { /** * Resizes and repositions videos in full screen mode. */ $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange', function () { VideoLayout.resizeLargeVideoContainer(); VideoLayout.positionLarge(); } ); $(window).resize(function () { VideoLayout.resizeLargeVideoContainer(); VideoLayout.positionLarge(); }); } UI.start = function (init) { document.title = interfaceConfig.APP_NAME; if(config.enableWelcomePage && window.location.pathname == "/" && (!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false")) { $("#videoconference_page").hide(); var setupWelcomePage = require("./welcome_page/WelcomePage"); setupWelcomePage(); return; } if (interfaceConfig.SHOW_JITSI_WATERMARK) { var leftWatermarkDiv = $("#largeVideoContainer div[class='watermark leftwatermark']"); leftWatermarkDiv.css({display: 'block'}); leftWatermarkDiv.parent().get(0).href = interfaceConfig.JITSI_WATERMARK_LINK; } if (interfaceConfig.SHOW_BRAND_WATERMARK) { var rightWatermarkDiv = $("#largeVideoContainer div[class='watermark rightwatermark']"); rightWatermarkDiv.css({display: 'block'}); rightWatermarkDiv.parent().get(0).href = interfaceConfig.BRAND_WATERMARK_LINK; rightWatermarkDiv.get(0).style.backgroundImage = "url(images/rightwatermark.png)"; } if (interfaceConfig.SHOW_POWERED_BY) { $("#largeVideoContainer>a[class='poweredby']").css({display: 'block'}); } $("#welcome_page").hide(); VideoLayout.resizeLargeVideoContainer(); $("#videospace").mousemove(function () { return ToolbarToggler.showToolbar(); }); // Set the defaults for prompt dialogs. jQuery.prompt.setDefaults({persistent: false}); VideoLayout.init(eventEmitter); AudioLevels.init(); NicknameHandler.init(eventEmitter); registerListeners(); bindEvents(); setupPrezi(); setupToolbars(); setupChat(); document.title = interfaceConfig.APP_NAME; $("#downloadlog").click(function (event) { dump(event.target); }); if(config.enableWelcomePage && window.location.pathname == "/" && (!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false")) { $("#videoconference_page").hide(); var setupWelcomePage = require("./welcome_page/WelcomePage"); setupWelcomePage(); return; } $("#welcome_page").hide(); // Display notice message at the top of the toolbar if (config.noticeMessage) { $('#noticeText').text(config.noticeMessage); $('#notice').css({display: 'block'}); } document.getElementById('largeVideo').volume = 0; if (!$('#settings').is(':visible')) { console.log('init'); init(); } else { loginInfo.onsubmit = function (e) { if (e.preventDefault) e.preventDefault(); $('#settings').hide(); init(); }; } toastr.options = { "closeButton": true, "debug": false, "positionClass": "notification-bottom-right", "onclick": null, "showDuration": "300", "hideDuration": "1000", "timeOut": "2000", "extendedTimeOut": "1000", "showEasing": "swing", "hideEasing": "linear", "showMethod": "fadeIn", "hideMethod": "fadeOut", "reposition": function() { if(PanelToggler.isVisible()) { $("#toast-container").addClass("notification-bottom-right-center"); } else { $("#toast-container").removeClass("notification-bottom-right-center"); } }, "newestOnTop": false }; SettingsMenu.init(); }; function chatAddError(errorMessage, originalText) { return Chat.chatAddError(errorMessage, originalText); }; function chatSetSubject(text) { return Chat.chatSetSubject(text); }; function updateChatConversation(from, displayName, message) { return Chat.updateChatConversation(from, displayName, message); }; function onMucJoined(jid, info) { Toolbar.updateRoomUrl(window.location.href); var meHTML = APP.translation.generateTranslatonHTML("me"); $("#localNick").html(Strophe.getResourceFromJid(jid) + " (" + meHTML + ")"); var settings = Settings.getSettings(); // Add myself to the contact list. ContactList.addContact(jid, settings.email || settings.uid); // Once we've joined the muc show the toolbar ToolbarToggler.showToolbar(); var displayName = !config.displayJids ? info.displayName : Strophe.getResourceFromJid(jid); if (displayName) onDisplayNameChanged('localVideoContainer', displayName); VideoLayout.mucJoined(); } function initEtherpad(name) { Etherpad.init(name); } function onMucMemberLeft(jid) { console.log('left.muc', jid); var displayName = $('#participant_' + Strophe.getResourceFromJid(jid) + '>.displayname').html(); messageHandler.notify(displayName,'notify.somebody', 'disconnected', 'notify.disconnected'); if(!config.startAudioMuted || config.startAudioMuted > APP.members.size()) UIUtil.playSoundNotification('userLeft'); // Need to call this with a slight delay, otherwise the element couldn't be // found for some reason. // XXX(gp) it works fine without the timeout for me (with Chrome 38). window.setTimeout(function () { var container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(jid)); if (container) { ContactList.removeContact(jid); VideoLayout.removeConnectionIndicator(jid); // hide here, wait for video to close before removing $(container).hide(); VideoLayout.resizeThumbnails(); } }, 10); VideoLayout.participantLeft(jid); }; function onLocalRoleChanged(jid, info, pres, isModerator) { console.info("My role changed, new role: " + info.role); onModeratorStatusChanged(isModerator); VideoLayout.showModeratorIndicator(); SettingsMenu.onRoleChanged(); if (isModerator) { Authentication.closeAuthenticationWindow(); messageHandler.notify(null, "notify.me", 'connected', "notify.moderator"); } } function onModeratorStatusChanged(isModerator) { Toolbar.showSipCallButton(isModerator); Toolbar.showRecordingButton( isModerator); //&& // FIXME: // Recording visible if // there are at least 2(+ 1 focus) participants //Object.keys(connection.emuc.members).length >= 3); } function onPasswordRequired(callback) { // password is required Toolbar.lockLockButton(); var message = '

'; message += APP.translation.translateString( "dialog.passwordRequired"); message += '

' + ''; messageHandler.openTwoButtonDialog(null, null, null, message, true, "dialog.Ok", function (e, v, m, f) {}, null, function (e, v, m, f) { if (v) { var lockKey = f.lockKey; if (lockKey) { Toolbar.setSharedKey(lockKey); callback(lockKey); } } }, ':input:first' ); } /** * The dialpad button is shown iff there is at least one member that supports * DTMF (e.g. jigasi). */ function onDtmfSupportChanged(dtmfSupport) { //TODO: enable when the UI is ready //Toolbar.showDialPadButton(dtmfSupport); } function onMucMemberJoined(jid, id, displayName) { messageHandler.notify(displayName,'notify.somebody', 'connected', 'notify.connected'); if(!config.startAudioMuted || config.startAudioMuted > APP.members.size()) UIUtil.playSoundNotification('userJoined'); // Add Peer's container VideoLayout.ensurePeerContainerExists(jid,id); } function onMucPresenceStatus( jid, info) { VideoLayout.setPresenceStatus( 'participant_' + Strophe.getResourceFromJid(jid), info.status); } function onMucRoleChanged(role, displayName) { VideoLayout.showModeratorIndicator(); if (role === 'moderator') { var messageKey, messageOptions = {}; if (!displayName) { messageKey = "notify.grantedToUnknown"; } else { messageKey = "notify.grantedTo"; messageOptions = {to: displayName}; } messageHandler.notify( displayName,'notify.somebody', 'connected', messageKey, messageOptions); } } function onAuthenticationRequired(intervalCallback) { Authentication.openAuthenticationDialog( roomName, intervalCallback, function () { Toolbar.authenticateClicked(); }); }; function onLastNChanged(oldValue, newValue) { if (config.muteLocalVideoIfNotInLastN) { setVideoMute(!newValue, { 'byUser': false }); } } UI.toggleSmileys = function () { Chat.toggleSmileys(); }; UI.getSettings = function () { return Settings.getSettings(); }; UI.toggleFilmStrip = function () { return BottomToolbar.toggleFilmStrip(); }; UI.toggleChat = function () { return BottomToolbar.toggleChat(); }; UI.toggleContactList = function () { return BottomToolbar.toggleContactList(); }; UI.inputDisplayNameHandler = function (value) { VideoLayout.inputDisplayNameHandler(value); }; UI.getLargeVideoState = function() { return VideoLayout.getLargeVideoState(); }; UI.generateRoomName = function() { if(roomName) return roomName; var roomnode = null; var path = window.location.pathname; // 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 roomnode = 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) { roomnode = path.substr(1).toLowerCase(); } else { var word = RoomNameGenerator.generateRoomWithoutSeparator(); roomnode = word.toLowerCase(); window.history.pushState('VideoChat', 'Room: ' + word, window.location.pathname + word); } } roomName = roomnode + '@' + config.hosts.muc; return roomName; }; UI.connectionIndicatorShowMore = function(id) { return VideoLayout.connectionIndicators[id].showMore(); }; UI.showLoginPopup = function(callback) { console.log('password is required'); var message = '

'; message += APP.translation.translateString( "dialog.passwordRequired"); message += '

' + '' + ''; UI.messageHandler.openTwoButtonDialog(null, null, null, message, true, "dialog.Ok", function (e, v, m, f) { if (v) { if (f.username !== null && f.password != null) { callback(f.username, f.password); } } }, null, null, ':input:first' ); } UI.checkForNicknameAndJoin = function () { Authentication.closeAuthenticationDialog(); Authentication.stopInterval(); var nick = null; if (config.useNicks) { nick = window.prompt('Your nickname (optional)'); } APP.xmpp.joinRoom(roomName, config.useNicks, nick); }; function dump(elem, filename) { elem = elem.parentNode; elem.download = filename || 'meetlog.json'; elem.href = 'data:application/json;charset=utf-8,\n'; var data = APP.xmpp.populateData(); var metadata = {}; metadata.time = new Date(); metadata.url = window.location.href; metadata.ua = navigator.userAgent; var log = APP.xmpp.getLogger(); if (log) { metadata.xmpp = log; } data.metadata = metadata; elem.href += encodeURIComponent(JSON.stringify(data, null, ' ')); return false; } UI.getRoomName = function () { return roomName; }; UI.setInitialMuteFromFocus = function (muteAudio, muteVideo) { if(muteAudio || muteVideo) notifyForInitialMute(); if(muteAudio) UI.setAudioMuted(true); if(muteVideo) UI.setVideoMute(true); } /** * Mutes/unmutes the local video. */ UI.toggleVideo = function () { setVideoMute(!APP.RTC.localVideo.isMuted()); }; /** * Mutes / unmutes audio for the local participant. */ UI.toggleAudio = function() { UI.setAudioMuted(!APP.RTC.localAudio.isMuted()); }; /** * Sets muted audio state for the local participant. */ UI.setAudioMuted = function (mute, earlyMute) { var audioMute = null; if(earlyMute) audioMute = function (mute, cb) { return APP.xmpp.sendAudioInfoPresence(mute, cb); }; else audioMute = function (mute, cb) { return APP.xmpp.setAudioMute(mute, cb); } if(!audioMute(mute, function () { VideoLayout.showLocalAudioIndicator(mute); UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled"); })) { // We still click the button. UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled"); return; } } UI.addListener = function (type, listener) { eventEmitter.on(type, listener); } UI.clickOnVideo = function (videoNumber) { var remoteVideos = $(".videocontainer:not(#mixedstream)"); if (remoteVideos.length > videoNumber) { remoteVideos[videoNumber].click(); } } //Used by torture UI.showToolbar = function () { return ToolbarToggler.showToolbar(); } //Used by torture UI.dockToolbar = function (isDock) { return ToolbarToggler.dockToolbar(isDock); } UI.setVideoMuteButtonsState = function (mute) { var video = $('#video'); var communicativeClass = "icon-camera"; var muteClass = "icon-camera icon-camera-disabled"; if (mute) { video.removeClass(communicativeClass); video.addClass(muteClass); } else { video.removeClass(muteClass); video.addClass(communicativeClass); } } UI.setVideoMute = setVideoMute; module.exports = UI; },{"../../service/RTC/RTCEvents":99,"../../service/RTC/StreamEventTypes":101,"../../service/connectionquality/CQEvents":104,"../../service/desktopsharing/DesktopSharingEventTypes":105,"../../service/members/Events":106,"../../service/xmpp/XMPPEvents":108,"./../settings/Settings":43,"./audio_levels/AudioLevels.js":12,"./authentication/Authentication":14,"./avatar/Avatar":16,"./etherpad/Etherpad.js":17,"./prezi/Prezi.js":18,"./side_pannels/SidePanelToggler":20,"./side_pannels/chat/Chat.js":21,"./side_pannels/contactlist/ContactList":25,"./side_pannels/settings/SettingsMenu":26,"./toolbars/BottomToolbar":27,"./toolbars/Toolbar":28,"./toolbars/ToolbarToggler":29,"./util/MessageHandler":31,"./util/NicknameHandler":32,"./util/UIUtil":33,"./videolayout/VideoLayout.js":35,"./welcome_page/RoomnameGenerator":36,"./welcome_page/WelcomePage":37,"events":1}],12:[function(require,module,exports){ var CanvasUtil = require("./CanvasUtils"); var ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d'); function initActiveSpeakerAudioLevels() { var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2; var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2; // Draw a circle. ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI); // Add a shadow around the circle ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR; ASDrawContext.shadowOffsetX = 0; ASDrawContext.shadowOffsetY = 0; } /** * The audio Levels plugin. */ var AudioLevels = (function(my) { var audioLevelCanvasCache = {}; my.LOCAL_LEVEL = 'local'; my.init = function () { initActiveSpeakerAudioLevels(); } /** * Updates the audio level canvas for the given peerJid. If the canvas * didn't exist we create it. */ my.updateAudioLevelCanvas = function (peerJid, VideoLayout) { var resourceJid = null; var videoSpanId = null; if (!peerJid) videoSpanId = 'localVideoContainer'; else { resourceJid = Strophe.getResourceFromJid(peerJid); videoSpanId = 'participant_' + resourceJid; } var videoSpan = document.getElementById(videoSpanId); if (!videoSpan) { if (resourceJid) console.error("No video element for jid", resourceJid); else console.error("No video element for local video."); return; } var audioLevelCanvas = $('#' + videoSpanId + '>canvas'); var videoSpaceWidth = $('#remoteVideos').width(); var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth); var thumbnailWidth = thumbnailSize[0]; var thumbnailHeight = thumbnailSize[1]; if (!audioLevelCanvas || audioLevelCanvas.length === 0) { audioLevelCanvas = document.createElement('canvas'); audioLevelCanvas.className = "audiolevel"; audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px"; audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px"; resizeAudioLevelCanvas( audioLevelCanvas, thumbnailWidth, thumbnailHeight); videoSpan.appendChild(audioLevelCanvas); } else { audioLevelCanvas = audioLevelCanvas.get(0); resizeAudioLevelCanvas( audioLevelCanvas, thumbnailWidth, thumbnailHeight); } }; /** * Updates the audio level UI for the given resourceJid. * * @param resourceJid the resource jid indicating the video element for * which we draw the audio level * @param audioLevel the newAudio level to render */ my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) { drawAudioLevelCanvas(resourceJid, audioLevel); var videoSpanId = getVideoSpanId(resourceJid); var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0); if (!audioLevelCanvas) return; var drawContext = audioLevelCanvas.getContext('2d'); var canvasCache = audioLevelCanvasCache[resourceJid]; drawContext.clearRect (0, 0, audioLevelCanvas.width, audioLevelCanvas.height); drawContext.drawImage(canvasCache, 0, 0); if(resourceJid === AudioLevels.LOCAL_LEVEL) { if(!APP.xmpp.myJid()) { return; } resourceJid = APP.xmpp.myResource(); } if(resourceJid === largeVideoResourceJid) { window.requestAnimationFrame(function () { AudioLevels.updateActiveSpeakerAudioLevel(audioLevel); }); } }; my.updateActiveSpeakerAudioLevel = function(audioLevel) { if($("#activeSpeaker").css("visibility") == "hidden") return; ASDrawContext.clearRect(0, 0, 300, 300); if(audioLevel == 0) 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; } /** * Draws the audio level canvas into the cached canvas object. * * @param resourceJid the resource jid indicating the video element for * which we draw the audio level * @param audioLevel the newAudio level to render */ function drawAudioLevelCanvas(resourceJid, audioLevel) { if (!audioLevelCanvasCache[resourceJid]) { var videoSpanId = getVideoSpanId(resourceJid); 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.heigh !== height + interfaceConfig.CANVAS_EXTRA) { canvas.height = height + interfaceConfig.CANVAS_EXTRA; resized = true; } }); 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; },{"./CanvasUtils":13}],13:[function(require,module,exports){ /** * Utility class for drawing canvas shapes. */ var CanvasUtil = (function(my) { /** * Draws a round rectangle with a glow. The glowWidth indicates the depth * of the glow. * * @param drawContext the context of the canvas to draw to * @param x the x coordinate of the round rectangle * @param y the y coordinate of the round rectangle * @param w the width of the round rectangle * @param h the height of the round rectangle * @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) { // Save the previous state of the context. drawContext.save(); if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; // Draw a round rectangle. drawContext.beginPath(); drawContext.moveTo(x+r, y); drawContext.arcTo(x+w, y, x+w, y+h, r); drawContext.arcTo(x+w, y+h, x, y+h, r); drawContext.arcTo(x, y+h, x, y, r); drawContext.arcTo(x, y, x+w, y, r); drawContext.closePath(); // Add a shadow around the rectangle drawContext.shadowColor = glowColor; drawContext.shadowBlur = glowWidth; drawContext.shadowOffsetX = 0; drawContext.shadowOffsetY = 0; // Fill the shape. drawContext.fill(); drawContext.save(); drawContext.restore(); // 1) Uncomment this line to use Composite Operation, which is doing the // same as the clip function below and is also antialiasing the round // border, but is said to be less fast performance wise. // drawContext.globalCompositeOperation='destination-out'; drawContext.beginPath(); drawContext.moveTo(x+r, y); drawContext.arcTo(x+w, y, x+w, y+h, r); drawContext.arcTo(x+w, y+h, x, y+h, r); drawContext.arcTo(x, y+h, x, y, r); drawContext.arcTo(x, y, x+w, y, r); drawContext.closePath(); // 2) Uncomment this line to use Composite Operation, which is doing the // same as the clip function below and is also antialiasing the round // border, but is said to be less fast performance wise. // drawContext.fill(); // Comment these two lines if choosing to do the same with composite // operation above 1 and 2. drawContext.clip(); drawContext.clearRect(0, 0, 277, 200); // Restore the previous context state. drawContext.restore(); }; /** * Clones the given canvas. * * @return the new cloned canvas. */ my.cloneCanvas = function (oldCanvas) { /* * FIXME Testing has shown that oldCanvas 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 (!oldCanvas) return oldCanvas; //create a new canvas var newCanvas = document.createElement('canvas'); var context = newCanvas.getContext('2d'); //set dimensions newCanvas.width = oldCanvas.width; newCanvas.height = oldCanvas.height; //apply the old canvas to the new one context.drawImage(oldCanvas, 0, 0); //return the new canvas return newCanvas; }; return my; })(CanvasUtil || {}); module.exports = CanvasUtil; },{}],14:[function(require,module,exports){ /* 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.generateTranslatonHTML("dialog.WaitingForHost"); var msg = APP.translation.generateTranslatonHTML( "dialog.WaitForHostMsg", {room: room}); var buttonTxt = APP.translation.generateTranslatonHTML("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.getMUCJoined()) { 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; },{"../../xmpp/moderator":53,"./LoginDialog":15}],15:[function(require,module,exports){ /* 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 /** * Creates new Dialog instance. * @param callback function(Strophe.Connection, Strophe.Status) called * when we either fail to connect or succeed(check Strophe.Status). * @param obtainSession true if we want to send ConferenceIQ to Jicofo * in order to create session-id after the connection is established. * @constructor */ function Dialog(callback, obtainSession) { var self = this; var stop = false; var connection = APP.xmpp.createConnection(); var message = '

'; message += APP.translation.translateString("dialog.passwordRequired"); message += '

' + '' + ''; var okButton = APP.translation.generateTranslatonHTML("dialog.Ok"); var cancelButton = APP.translation.generateTranslatonHTML("dialog.Cancel"); var states = { login: { html: message, buttons: [ { title: okButton, value: true}, { title: cancelButton, value: false} ], focus: ':input:first', submit: function (e, v, m, f) { e.preventDefault(); if (v) { var jid = f.username; var password = f.password; if (jid && password) { stop = false; connection.reset(); connDialog.goToState('connecting'); connection.connect(jid, password, stateHandler); } } else { // User cancelled stop = true; callback(); } } }, connecting: { title: APP.translation.translateString('dialog.connecting'), html: '
', buttons: [], defaultButton: 0 }, finished: { title: APP.translation.translateString('dialog.error'), html: '
', buttons: [ { title: APP.translation.translateString('dialog.retry'), value: 'retry' }, { title: APP.translation.translateString('dialog.Cancel'), value: 'cancel' }, ], defaultButton: 0, submit: function (e, v, m, f) { e.preventDefault(); if (v === 'retry') connDialog.goToState('login'); else callback(); } } }; 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; } }; /** * Displays error message in 'finished' state which allows either to cancel * or retry. * @param message the final message to be displayed. */ this.displayError = function (message) { var finishedState = connDialog.getState('finished'); var errorMessageElem = finishedState.find('#errorMessage'); errorMessageElem.text(message); connDialog.goToState('finished'); }; /** * Closes LoginDialog. */ this.close = function () { stop = true; connDialog.close(); }; } var LoginDialog = { /** * Displays login prompt used to establish new XMPP connection. Given * callback(Strophe.Connection, Strophe.Status) 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 function(Strophe.Connection, Strophe.Status) * called when we either fail to connect or succeed(check * Strophe.Status). * @param obtainSession true if we want to send ConferenceIQ to * Jicofo in order to create session-id after the connection is * established. * @returns {Dialog} */ show: function (callback, obtainSession) { return new Dialog(callback, obtainSession); } }; module.exports = LoginDialog; },{"../../xmpp/moderator":53,"../../xmpp/xmpp":61}],16:[function(require,module,exports){ var Settings = require("../../settings/Settings"); var MediaStreamType = require("../../../service/RTC/MediaStreamTypes"); var users = {}; var activeSpeakerJid; function setVisibility(selector, show) { if (selector && selector.length > 0) { selector.css("visibility", show ? "visible" : "hidden"); } } function isUserMuted(jid) { // XXX(gp) we may want to rename this method to something like // isUserStreaming, for example. if (jid && jid != APP.xmpp.myJid()) { var resource = Strophe.getResourceFromJid(jid); if (!require("../videolayout/VideoLayout").isInLastN(resource)) { return true; } } if(jid && jid == APP.xmpp.myJid()) { var localVideo = APP.RTC.localVideo; return (!localVideo || localVideo.isMuted()); } if (!APP.RTC.remoteStreams[jid] || !APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) { return null; } return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted; } function getGravatarUrl(id, size) { if(id === APP.xmpp.myJid() || !id) { id = Settings.getSettings().uid; } return 'https://www.gravatar.com/avatar/' + MD5.hexdigest(id.trim().toLowerCase()) + "?d=wavatar&size=" + (size || "30"); } var Avatar = { /** * 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 */ setUserAvatar: function (jid, id) { if (id) { if (users[jid] === id) { return; } users[jid] = id; } var thumbUrl = getGravatarUrl(users[jid] || jid, 100); var contactListUrl = getGravatarUrl(users[jid] || jid); var resourceJid = Strophe.getResourceFromJid(jid); var thumbnail = $('#participant_' + resourceJid); var avatar = $('#avatar_' + resourceJid); // set the avatar in the settings menu if it is local user and get the // local video container if (jid === APP.xmpp.myJid()) { $('#avatar').get(0).src = thumbUrl; thumbnail = $('#localVideoContainer'); } // set the avatar in the contact list var contact = $('#' + resourceJid + '>img'); if (contact && contact.length > 0) { contact.get(0).src = contactListUrl; } // set the avatar in the thumbnail if (avatar && avatar.length > 0) { avatar[0].src = thumbUrl; } else { if (thumbnail && thumbnail.length > 0) { avatar = document.createElement('img'); avatar.id = 'avatar_' + resourceJid; avatar.className = 'userAvatar'; avatar.src = thumbUrl; thumbnail.append(avatar); } } //if the user is the current active speaker - update the active speaker // avatar if (jid === activeSpeakerJid) { this.updateActiveSpeakerAvatarSrc(jid); } }, /** * Hides or shows the user's avatar * @param jid jid of the user * @param show whether we should show the avatar or not * video because there is no dominant speaker and no focused speaker */ showUserAvatar: function (jid, show) { if (users[jid]) { var resourceJid = Strophe.getResourceFromJid(jid); var video = $('#participant_' + resourceJid + '>video'); var avatar = $('#avatar_' + resourceJid); if (jid === APP.xmpp.myJid()) { video = $('#localVideoWrapper>video'); } if (show === undefined || show === null) { show = isUserMuted(jid); } //if the user is the currently focused, the dominant speaker or if //there is no focused and no dominant speaker and the large video is //currently shown if (activeSpeakerJid === jid && require("../videolayout/VideoLayout").isLargeVideoOnTop()) { setVisibility($("#largeVideo"), !show); setVisibility($('#activeSpeaker'), show); setVisibility(avatar, false); setVisibility(video, false); } else { if (video && video.length > 0) { setVisibility(video, !show); } setVisibility(avatar, show); } } }, /** * Updates the src of the active speaker avatar * @param jid of the current active speaker */ updateActiveSpeakerAvatarSrc: function (jid) { if (!jid) { jid = APP.xmpp.findJidFromResource( require("../videolayout/VideoLayout").getLargeVideoState().userResourceJid); } var avatar = $("#activeSpeakerAvatar")[0]; var url = getGravatarUrl(users[jid], interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE); if (jid === activeSpeakerJid && avatar.src === url) { return; } activeSpeakerJid = jid; var isMuted = isUserMuted(jid); if (jid && isMuted !== null) { avatar.src = url; setVisibility($("#largeVideo"), !isMuted); Avatar.showUserAvatar(jid, isMuted); } } }; module.exports = Avatar; },{"../../../service/RTC/MediaStreamTypes":97,"../../settings/Settings":43,"../videolayout/VideoLayout":35}],17:[function(require,module,exports){ /* 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"; /** * Resizes the etherpad. */ 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 (!$('#etherpadButton').is(":visible")) $('#etherpadButton').css({display: 'inline-block'}); } /** * Creates the IFrame for the etherpad. */ function createIFrame() { etherpadIFrame = document.createElement('iframe'); etherpadIFrame.src = domain + etherpadName + options; etherpadIFrame.frameBorder = 0; etherpadIFrame.scrolling = "no"; etherpadIFrame.width = $('#largeVideoContainer').width() || 640; etherpadIFrame.height = $('#largeVideoContainer').height() || 480; etherpadIFrame.setAttribute('style', 'visibility: hidden;'); document.getElementById('etherpad').appendChild(etherpadIFrame); etherpadIFrame.onload = function() { document.domain = document.domain; bubbleIframeMouseMove(etherpadIFrame); setTimeout(function() { // the iframes inside of the etherpad are // not yet loaded when the etherpad iframe is loaded var outer = etherpadIFrame. contentDocument.getElementsByName("ace_outer")[0]; bubbleIframeMouseMove(outer); var inner = outer. contentDocument.getElementsByName("ace_inner")[0]; bubbleIframeMouseMove(inner); }, 2000); }; } function bubbleIframeMouseMove(iframe){ var existingOnMouseMove = iframe.contentWindow.onmousemove; iframe.contentWindow.onmousemove = function(e){ if(existingOnMouseMove) existingOnMouseMove(e); var evt = document.createEvent("MouseEvents"); var boundingClientRect = iframe.getBoundingClientRect(); evt.initMouseEvent( "mousemove", true, // bubbles false, // not cancelable window, e.detail, e.screenX, e.screenY, e.clientX + boundingClientRect.left, e.clientY + boundingClientRect.top, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, null // no related element ); iframe.dispatchEvent(evt); }; } /** * On video selected event. */ $(document).bind('video.selected', function (event, isPresentation) { if (config.etherpad_base && etherpadIFrame && etherpadIFrame.style.visibility !== 'hidden') Etherpad.toggleEtherpad(isPresentation); }); var Etherpad = { /** * Initializes the etherpad. */ init: function (name) { if (config.etherpad_base && !etherpadName && name) { domain = config.etherpad_base; etherpadName = name; enableEtherpadButton(); /** * Resizes the etherpad, when the window is resized. */ $(window).resize(function () { resize(); }); } }, /** * Opens/hides the Etherpad. */ toggleEtherpad: function (isPresentation) { if (!etherpadIFrame) createIFrame(); var largeVideo = null; if (Prezi.isPresentationVisible()) largeVideo = $('#presentation>iframe'); else largeVideo = $('#largeVideo'); if ($('#etherpad>iframe').css('visibility') === 'hidden') { $('#activeSpeaker').css('visibility', 'hidden'); largeVideo.fadeOut(300, function () { if (Prezi.isPresentationVisible()) { largeVideo.css({opacity: '0'}); } else { VideoLayout.setLargeVideoVisible(false); } }); $('#etherpad>iframe').fadeIn(300, function () { document.body.style.background = '#eeeeee'; $('#etherpad>iframe').css({visibility: 'visible'}); $('#etherpad').css({zIndex: 2}); }); } else if ($('#etherpad>iframe')) { $('#etherpad>iframe').fadeOut(300, function () { $('#etherpad>iframe').css({visibility: 'hidden'}); $('#etherpad').css({zIndex: 0}); document.body.style.background = 'black'; }); if (!isPresentation) { $('#largeVideo').fadeIn(300, function () { VideoLayout.setLargeVideoVisible(true); }); } } resize(); }, isVisible: function() { var etherpadIframe = $('#etherpad>iframe'); return etherpadIframe && etherpadIframe.is(':visible'); } }; module.exports = Etherpad; },{"../prezi/Prezi":18,"../util/UIUtil":33,"../videolayout/VideoLayout":35}],18:[function(require,module,exports){ 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; var Prezi = { /** * Reloads the current presentation. */ reloadPresentation: function() { var iframe = document.getElementById(preziPlayer.options.preziId); iframe.src = iframe.src; }, /** * Returns true if the presentation is visible, false - * 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.generateTranslatonHTML( "dialog.sharePreziTitle"); var cancelButton = APP.translation.generateTranslatonHTML( "dialog.Cancel"); var shareButton = APP.translation.generateTranslatonHTML( "dialog.Share"); var backButton = APP.translation.generateTranslatonHTML( "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.generateTranslatonHTML( "dialog.preziLinkError"); var defaultUrl = APP.translation.translateString("defaultPreziLink", {url: "http://prezi.com/wz7vhjycl7e6/my-prezi"}); var openPreziState = { state0: { html: '

' + html + '

' + '', 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: '

' + html + '

' + linkError, persistent: false, buttons: buttons1, focus: ':input:first', defaultButton: 1, submit: function (e, v, m, f) { e.preventDefault(); if (v === 0) $.prompt.close(); else $.prompt.goToState('state0'); } } }; messageHandler.openDialogWithStates(openPreziState); } } }; /** * A new presentation has been added. * * @param event the event indicating the add of a presentation * @param jid the jid from which the presentation was added * @param presUrl url of the presentation * @param currentSlide the current slide to which we should move */ function presentationAdded(event, jid, presUrl, currentSlide) { console.log("presentation added", presUrl); var presId = getPresentationId(presUrl); var elementId = 'participant_' + Strophe.getResourceFromJid(jid) + '_' + presId; // We explicitly don't specify the peer jid here, because we don't want // this video to be dealt with as a peer related one (for example we // don't want to show a mute/kick menu for this one, etc.). VideoLayout.addRemoteVideoContainer(null, elementId); VideoLayout.resizeThumbnails(); var controlsEnabled = false; if (jid === APP.xmpp.myJid()) controlsEnabled = true; setPresentationVisible(true); $('#largeVideoContainer').hover( function (event) { if (Prezi.isPresentationVisible()) { var reloadButtonRight = window.innerWidth - $('#presentation>iframe').offset().left - $('#presentation>iframe').width(); $('#reloadPresentation').css({ right: reloadButtonRight, display:'inline-block'}); } }, function (event) { if (!Prezi.isPresentationVisible()) $('#reloadPresentation').css({display:'none'}); else { var e = event.toElement || event.relatedTarget; if (e && e.id != 'reloadPresentation' && e.id != 'header') $('#reloadPresentation').css({display:'none'}); } }); preziPlayer = new PreziPlayer( 'presentation', {preziId: presId, width: getPresentationWidth(), height: getPresentationHeihgt(), controls: controlsEnabled, debug: true }); $('#presentation>iframe').attr('id', preziPlayer.options.preziId); preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) { console.log("prezi status", event.value); if (event.value == PreziPlayer.STATUS_CONTENT_READY) { if (jid != APP.xmpp.myJid()) preziPlayer.flyToStep(currentSlide); } }); preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) { console.log("event value", event.value); APP.xmpp.addToPresence("preziSlide", event.value); }); $("#" + elementId).css( 'background-image', 'url(../images/avatarprezi.png)'); $("#" + elementId).click( function () { setPresentationVisible(true); } ); }; /** * A presentation has been removed. * * @param event the event indicating the remove of a presentation * @param jid the jid for which the presentation was removed * @param the url of the presentation */ function presentationRemoved(event, jid, presUrl) { console.log('presentation removed', presUrl); var presId = getPresentationId(presUrl); setPresentationVisible(false); $('#participant_' + Strophe.getResourceFromJid(jid) + '_' + presId).remove(); $('#presentation>iframe').remove(); if (preziPlayer != null) { preziPlayer.destroy(); preziPlayer = null; } }; /** * Indicates if the given string is an alphanumeric string. * Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the * purpose of checking URIs. */ function isAlphanumeric(unsafeText) { var regex = /^[a-z0-9-_\/&\?=;]+$/i; return regex.test(unsafeText); } /** * Returns the presentation id from the given url. */ function getPresentationId (presUrl) { var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10); return presIdTmp.substring(0, presIdTmp.indexOf('/')); } /** * Returns the presentation width. */ function getPresentationWidth() { var availableWidth = UIUtil.getAvailableVideoWidth(); var availableHeight = getPresentationHeihgt(); var aspectRatio = 16.0 / 9.0; if (availableHeight < availableWidth / aspectRatio) { availableWidth = Math.floor(availableHeight * aspectRatio); } return availableWidth; } /** * Returns the presentation height. */ 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()); } } /** * Shows/hides a presentation. */ function setPresentationVisible(visible) { var prezi = $('#presentation>iframe'); if (visible) { // Trigger the video.selected event to indicate a change in the // large video. $(document).trigger("video.selected", [true]); $('#largeVideo').fadeOut(300); prezi.fadeIn(300, function() { prezi.css({opacity:'1'}); ToolbarToggler.dockToolbar(true); VideoLayout.setLargeVideoVisible(false); }); $('#activeSpeaker').css('visibility', 'hidden'); } else { if (prezi.css('opacity') == '1') { prezi.fadeOut(300, function () { prezi.css({opacity:'0'}); $('#reloadPresentation').css({display:'none'}); $('#largeVideo').fadeIn(300, function() { VideoLayout.setLargeVideoVisible(true); ToolbarToggler.dockToolbar(false); }); }); } } } /** * Presentation has been removed. */ $(document).bind('presentationremoved.muc', presentationRemoved); /** * Presentation has been added. */ $(document).bind('presentationadded.muc', presentationAdded); /* * Indicates presentation slide change. */ $(document).bind('gotoslide.muc', function (event, jid, presUrl, current) { if (preziPlayer && preziPlayer.getCurrentStep() != current) { preziPlayer.flyToStep(current); var animationStepsArray = preziPlayer.getAnimationCountOnSteps(); for (var i = 0; i < parseInt(animationStepsArray[current]); i++) { preziPlayer.flyToStep(current, i); } } }); /** * On video selected event. */ $(document).bind('video.selected', function (event, isPresentation) { if (!isPresentation && $('#presentation>iframe')) { setPresentationVisible(false); } }); $(window).resize(function () { resize(); }); module.exports = Prezi; },{"../toolbars/ToolbarToggler":29,"../util/MessageHandler":31,"../util/UIUtil":33,"../videolayout/VideoLayout":35,"./PreziPlayer":19}],19:[function(require,module,exports){ (function() { "use strict"; var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; window.PreziPlayer = (function() { 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 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 == true) { 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); } return PreziPlayer; })(); })(); module.exports = PreziPlayer; },{}],20:[function(require,module,exports){ 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"); /** * Toggler for the chat, contact list, settings menu, etc.. */ var PanelToggler = (function(my) { var currentlyOpen = null; var buttons = { '#chatspace': '#chatBottomButton', '#contactlist': '#contactListButton', '#settingsmenu': '#settingsButton' }; /** * Resizes the video area * @param isClosing whether the side panel is going to be closed or is going to open / remain opened * @param completeFunction a function to be called when the video space is resized */ var resizeVideoArea = function(isClosing, completeFunction) { var videospace = $('#videospace'); var panelSize = isClosing ? [0, 0] : PanelToggler.getPanelSize(); var videospaceWidth = window.innerWidth - panelSize[0]; var videospaceHeight = window.innerHeight; var videoSize = VideoLayout.getVideoSize(null, null, videospaceWidth, videospaceHeight); var videoWidth = videoSize[0]; var videoHeight = videoSize[1]; var videoPosition = VideoLayout.getVideoPosition(videoWidth, videoHeight, videospaceWidth, videospaceHeight); var horizontalIndent = videoPosition[0]; var verticalIndent = videoPosition[1]; var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth); var thumbnailsWidth = thumbnailSize[0]; var thumbnailsHeight = thumbnailSize[1]; //for chat videospace.animate({ right: panelSize[0], width: videospaceWidth, height: videospaceHeight }, { queue: false, duration: 500, complete: completeFunction }); $('#remoteVideos').animate({ height: thumbnailsHeight }, { queue: false, duration: 500 }); $('#remoteVideos>span').animate({ height: thumbnailsHeight, width: thumbnailsWidth }, { queue: false, duration: 500, complete: function () { $(document).trigger( "remotevideo.resized", [thumbnailsWidth, thumbnailsHeight]); } }); $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight }, { queue: false, duration: 500 }); $('#largeVideo').animate({ width: videoWidth, height: videoHeight, top: verticalIndent, bottom: verticalIndent, left: horizontalIndent, right: horizontalIndent }, { queue: false, duration: 500 }); }; /** * 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 (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: (PanelToggler.getPanelSize()[0] + 5) + 'px' }, { queue: false, duration: 500 }); $(selector).show("slide", { direction: "right", queue: false, duration: 500, complete: onOpenComplete }); if(typeof onOpen === "function") { onOpen(); } currentlyOpen = selector; } }; /** * Opens / closes the chat area. */ my.toggleChat = function() { var chatCompleteFunction = Chat.isVisible() ? function() {} : function () { Chat.scrollChatToBottom(); $('#chatspace').trigger('shown'); }; resizeVideoArea(Chat.isVisible(), chatCompleteFunction); toggle(Chat, '#chatspace', function () { // Request the focus in the nickname field or the chat input field. if ($('#nickname').css('visibility') === 'visible') { $('#nickinput').focus(); } else { $('#usermsg').focus(); } }, null, Chat.resizeChat, null); }; /** * Opens / closes the contact list area. */ my.toggleContactList = function () { var completeFunction = ContactList.isVisible() ? function() {} : function () { $('#contactlist').trigger('shown');}; resizeVideoArea(ContactList.isVisible(), completeFunction); toggle(ContactList, '#contactlist', null, function() { ContactList.setVisualNotification(false); }, null); }; /** * Opens / closes the settings menu */ my.toggleSettingsMenu = function() { resizeVideoArea(SettingsMenu.isVisible(), function (){}); toggle(SettingsMenu, '#settingsmenu', null, function() { var settings = Settings.getSettings(); $('#setDisplayName').get(0).value = settings.displayName; $('#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() { return (Chat.isVisible() || ContactList.isVisible() || SettingsMenu.isVisible()); }; return my; }(PanelToggler || {})); module.exports = PanelToggler; },{"../toolbars/ToolbarToggler":29,"../util/UIUtil":33,"../videolayout/VideoLayout":35,"./../../settings/Settings":43,"./chat/Chat":21,"./contactlist/ContactList":25,"./settings/SettingsMenu":26}],21:[function(require,module,exports){ /* global $, Util, nickname:true */ var Replacement = require("./Replacement"); var CommandsProcessor = require("./Commands"); var ToolbarToggler = require("../../toolbars/ToolbarToggler"); 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; /** * Shows/hides a visual notification, indicating that a message has arrived. */ function setVisualNotification(show) { var unreadMsgElement = document.getElementById('unreadMessages'); var unreadMsgBottomElement = document.getElementById('bottomUnreadMessages'); var glower = $('#chatButton'); var bottomGlower = $('#chatBottomButton'); if (unreadMessages) { unreadMsgElement.innerHTML = unreadMessages.toString(); unreadMsgBottomElement.innerHTML = unreadMessages.toString(); ToolbarToggler.dockToolbar(true); var chatButtonElement = document.getElementById('chatButton').parentNode; var leftIndent = (UIUtil.getTextWidth(chatButtonElement) - UIUtil.getTextWidth(unreadMsgElement)) / 2; var topIndent = (UIUtil.getTextHeight(chatButtonElement) - UIUtil.getTextHeight(unreadMsgElement)) / 2 - 3; unreadMsgElement.setAttribute( 'style', 'top:' + topIndent + '; left:' + leftIndent + ';'); var chatBottomButtonElement = document.getElementById('chatBottomButton').parentNode; var bottomLeftIndent = (UIUtil.getTextWidth(chatBottomButtonElement) - UIUtil.getTextWidth(unreadMsgBottomElement)) / 2; var bottomTopIndent = (UIUtil.getTextHeight(chatBottomButtonElement) - UIUtil.getTextHeight(unreadMsgBottomElement)) / 2 - 2; unreadMsgBottomElement.setAttribute( 'style', 'top:' + bottomTopIndent + '; left:' + bottomLeftIndent + ';'); if (!glower.hasClass('icon-chat-simple')) { glower.removeClass('icon-chat'); glower.addClass('icon-chat-simple'); } } else { unreadMsgElement.innerHTML = ''; unreadMsgBottomElement.innerHTML = ''; glower.removeClass('icon-chat-simple'); glower.addClass('icon-chat'); } if (show && !notificationInterval) { notificationInterval = window.setInterval(function () { glower.toggleClass('active'); bottomGlower.toggleClass('active glowing'); }, 800); } else if (!show && notificationInterval) { window.clearInterval(notificationInterval); notificationInterval = false; glower.removeClass('active'); bottomGlower.removeClass('glowing'); bottomGlower.addClass('active'); } } /** * Returns the current time in the format it is shown to the user * @returns {string} */ function getCurrentTime() { var now = new Date(); var hour = now.getHours(); var minute = now.getMinutes(); var second = now.getSeconds(); if(hour.toString().length === 1) { hour = '0'+hour; } if(minute.toString().length === 1) { minute = '0'+minute; } if(second.toString().length === 1) { second = '0'+second; } return hour+':'+minute+':'+second; } function toggleSmileys() { var smileys = $('#smileysContainer'); if(!smileys.is(':visible')) { smileys.show("slide", { direction: "down", duration: 300}); } else { smileys.hide("slide", { direction: "down", duration: 300}); } $('#usermsg').focus(); } function addClickFunction(smiley, number) { smiley.onclick = function addSmileyToMessage() { var usermsg = $('#usermsg'); var message = usermsg.val(); message += smileys['smiley' + number]; usermsg.val(message); usermsg.get(0).setSelectionRange(message.length, message.length); toggleSmileys(); usermsg.focus(); }; } /** * Adds the smileys container to the chat */ function addSmileys() { var smileysContainer = document.createElement('div'); smileysContainer.id = 'smileysContainer'; for(var i = 1; i <= 21; i++) { var smileyContainer = document.createElement('div'); smileyContainer.id = 'smiley' + i; smileyContainer.className = 'smileyContainer'; var smiley = document.createElement('img'); smiley.src = 'images/smileys/smiley' + i + '.svg'; smiley.className = 'smiley'; addClickFunction(smiley, i); smileyContainer.appendChild(smiley); smileysContainer.appendChild(smileyContainer); } $("#chatspace").append(smileysContainer); } /** * Resizes the chat conversation. */ function resizeChatConversation() { var msgareaHeight = $('#usermsg').outerHeight(); var chatspace = $('#chatspace'); var width = chatspace.width(); var chat = $('#chatconversation'); var smileys = $('#smileysarea'); smileys.height(msgareaHeight); $("#smileys").css('bottom', (msgareaHeight - 26) / 2); $('#smileysContainer').css('bottom', msgareaHeight); chat.width(width - 10); chat.height(window.innerHeight - 15 - msgareaHeight); } /** * Chat related user interface. */ var Chat = (function (my) { /** * Initializes chat related interface. */ my.init = function () { if(NicknameHandler.getNickname()) 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; } } }); $('#usermsg').keydown(function (event) { if (event.keyCode === 13) { event.preventDefault(); var value = this.value; $('#usermsg').val('').trigger('autosize.resize'); this.focus(); var command = new CommandsProcessor(value); if(command.isCommand()) { command.processCommand(); } else { var message = UIUtil.escapeHtml(value); APP.xmpp.sendChatMessage(message, NicknameHandler.getNickname()); } } }); var onTextAreaResize = function () { resizeChatConversation(); Chat.scrollChatToBottom(); }; $('#usermsg').autosize({callback: onTextAreaResize}); $("#chatspace").bind("shown", function () { unreadMessages = 0; setVisualNotification(false); }); addSmileys(); }; /** * Appends the given message to the chat conversation. */ my.updateChatConversation = function (from, displayName, message) { var divClassName = ''; if (APP.xmpp.myJid() === from) { divClassName = "localuser"; } else { divClassName = "remoteuser"; if (!Chat.isVisible()) { unreadMessages++; UIUtil.playSoundNotification('chatNotification'); setVisualNotification(true); } } // replace links and smileys // Strophe already escapes special symbols on sending, // so we escape here only tags to avoid double & var escMessage = message.replace(//g, '>').replace(/\n/g, '
'); var escDisplayName = UIUtil.escapeHtml(displayName); message = Replacement.processReplacements(escMessage); var messageContainer = '
'+ '' + '
' + escDisplayName + '
' + '
' + getCurrentTime() + '
' + '
' + message + '
' + '
'; $('#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) { errorMessage = UIUtil.escapeHtml(errorMessage); originalText = UIUtil.escapeHtml(originalText); $('#chatconversation').append( '
Error: ' + 'Your message' + (originalText? (' \"'+ originalText + '\"') : "") + ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '') + '
'); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); }; /** * Sets the subject to the UI * @param subject the subject */ my.chatSetSubject = function(subject) { if(subject) subject = subject.trim(); $('#subject').html(Replacement.linkify(UIUtil.escapeHtml(subject))); if(subject === "") { $("#subject").css({display: "none"}); } else { $("#subject").css({display: "block"}); } }; /** * Sets the chat conversation mode. */ my.setChatConversationMode = function (isConversationMode) { if (isConversationMode) { $('#nickname').css({visibility: 'hidden'}); $('#chatconversation').css({visibility: 'visible'}); $('#usermsg').css({visibility: 'visible'}); $('#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]); resizeChatConversation(); }; /** * Indicates if the chat is currently visible. */ my.isVisible = function () { return $('#chatspace').is(":visible"); }; /** * Shows and hides the window with the smileys */ my.toggleSmileys = toggleSmileys; /** * Scrolls chat to the bottom. */ my.scrollChatToBottom = function() { setTimeout(function () { $('#chatconversation').scrollTop( $('#chatconversation')[0].scrollHeight); }, 5); }; return my; }(Chat || {})); module.exports = Chat; },{"../../../../service/UI/UIEvents":102,"../../toolbars/ToolbarToggler":29,"../../util/NicknameHandler":32,"../../util/UIUtil":33,"../SidePanelToggler":20,"./Commands":22,"./Replacement":23,"./smileys.json":24}],22:[function(require,module,exports){ var UIUtil = require("../../util/UIUtil"); /** * 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 = { "topic" : processTopic }; /** * Extracts the command from the message. * @param message the received message * @returns {string} the command */ function getCommand(message) { if(message) { for(var command in commands) { if(message.indexOf("/" + command) == 0) return command; } } return ""; }; /** * Processes the data for topic command. * @param commandArguments the arguments of the topic command. */ function processTopic(commandArguments) { var topic = UIUtil.escapeHtml(commandArguments); APP.xmpp.setSubject(topic); } /** * Constructs new CommandProccessor instance from a message that * handles commands received via chat messages. * @param message the message * @constructor */ function CommandsProcessor(message) { var command = getCommand(message); /** * Returns the name of the command. * @returns {String} the command */ this.getCommand = function() { return command; }; var messageArgument = message.substr(command.length + 2); /** * Returns the arguments of the command. * @returns {string} */ this.getArgument = function() { return messageArgument; }; } /** * Checks whether this instance is valid command or not. * @returns {boolean} */ CommandsProcessor.prototype.isCommand = function() { if(this.getCommand()) return true; return false; }; /** * Processes the command. */ CommandsProcessor.prototype.processCommand = function() { if(!this.isCommand()) return; commands[this.getCommand()](this.getArgument()); }; module.exports = CommandsProcessor; },{"../../util/UIUtil":33}],23:[function(require,module,exports){ var Smileys = require("./smileys.json"); /** * Processes links and smileys in "body" */ function processReplacements(body) { //make links clickable body = linkify(body); //add smileys body = smilify(body); return body; } /** * Finds and replaces all links in the links in "body" * with their */ function linkify(inputText) { var replacedText, replacePattern1, replacePattern2, replacePattern3; //URLs starting with http://, https://, or ftp:// replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; replacedText = inputText.replace(replacePattern1, '$1'); //URLs starting with "www." (without // before it, or it'd re-link the ones done above). replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; replacedText = replacedText.replace(replacePattern2, '$1$2'); //Change email addresses to mailto:: links. replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim; replacedText = replacedText.replace(replacePattern3, '$1'); return replacedText; } /** * Replaces common smiley strings with images */ function smilify(body) { if(!body) { return body; } var regexs = Smileys["regexs"]; for(var smiley in regexs) { if(regexs.hasOwnProperty(smiley)) { body = body.replace(regexs[smiley], ''); } } return body; } module.exports = { processReplacements: processReplacements, linkify: linkify }; },{"./smileys.json":24}],24:[function(require,module,exports){ module.exports={ "smileys": { "smiley1": ":)", "smiley2": ":(", "smiley3": ":D", "smiley4": "(y)", "smiley5": " :P", "smiley6": "(wave)", "smiley7": "(blush)", "smiley8": "(chuckle)", "smiley9": "(shocked)", "smiley10": ":*", "smiley11": "(n)", "smiley12": "(search)", "smiley13": " <3", "smiley14": "(oops)", "smiley15": "(angry)", "smiley16": "(angel)", "smiley17": "(sick)", "smiley18": ";(", "smiley19": "(bomb)", "smiley20": "(clap)", "smiley21": " ;)" }, "regexs": { "smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi, "smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi, "smiley1": /(:-\)|:\))/gi, "smiley4": /(\(y\)|\(Y\)|\(ok\))/gi, "smiley5": /(:-P|:P|:-p|:p)/gi, "smiley6": /(\(wave\))/gi, "smiley7": /(\(blush\))/gi, "smiley8": /(\(chuckle\))/gi, "smiley9": /(:-0|\(shocked\))/gi, "smiley10": /(:-\*|:\*|\(kiss\))/gi, "smiley11": /(\(n\))/gi, "smiley12": /(\(search\))/g, "smiley13": /(<3|<3|&lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi, "smiley14": /(\(oops\))/gi, "smiley15": /(\(angry\))/gi, "smiley16": /(\(angel\))/gi, "smiley17": /(\(sick\))/gi, "smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi, "smiley19": /(\(bomb\))/gi, "smiley20": /(\(clap\))/gi, "smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi } } },{}],25:[function(require,module,exports){ var numberOfContacts = 0; var notificationInterval; /** * Updates the number of participants in the contact list button and sets * the glow * @param delta indicates whether a new user has joined (1) or someone has * left(-1) */ function updateNumberOfParticipants(delta) { //when the user is alone we don't show the number of participants if(numberOfContacts === 0) { $("#numberOfParticipants").text(''); numberOfContacts += delta; } else if(numberOfContacts !== 0 && !ContactList.isVisible()) { ContactList.setVisualNotification(true); numberOfContacts += delta; $("#numberOfParticipants").text(numberOfContacts); } } /** * Creates the avatar element. * * @return the newly created avatar element */ function createAvatar(id) { var avatar = document.createElement('img'); avatar.className = "icon-avatar avatar"; avatar.src = "https://www.gravatar.com/avatar/" + id + "?d=wavatar&size=30"; return avatar; } /** * Creates the display name paragraph. * * @param displayName the display name to set */ function createDisplayNameParagraph(key, displayName) { var p = document.createElement('p'); if(displayName) p.innerText = displayName; else if(key) { p.setAttribute("data-i18n",key); p.innerText = APP.translation.translateString(key); } return p; } function stopGlowing(glower) { window.clearInterval(notificationInterval); notificationInterval = false; glower.removeClass('glowing'); if (!ContactList.isVisible()) { glower.removeClass('active'); } } /** * Contact list. */ var ContactList = { /** * Indicates if the chat is currently visible. * * @return true if the chat is currently visible, false - * otherwise */ isVisible: function () { return $('#contactlist').is(":visible"); }, /** * Adds a contact for the given peerJid if such doesn't yet exist. * * @param peerJid the peerJid corresponding to the contact * @param id the user's email or userId used to get the user's avatar */ ensureAddContact: function (peerJid, id) { var resourceJid = Strophe.getResourceFromJid(peerJid); var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); if (!contact || contact.length <= 0) ContactList.addContact(peerJid, id); }, /** * Adds a contact for the given peer jid. * * @param peerJid the jid of the contact to add * @param id the email or userId of the user */ addContact: function (peerJid, id) { var resourceJid = Strophe.getResourceFromJid(peerJid); var contactlist = $('#contactlist>ul'); var newContact = document.createElement('li'); newContact.id = resourceJid; newContact.className = "clickable"; newContact.onclick = function (event) { if (event.currentTarget.className === "clickable") { $(ContactList).trigger('contactclicked', [peerJid]); } }; newContact.appendChild(createAvatar(id)); newContact.appendChild(createDisplayNameParagraph("participant")); var clElement = contactlist.get(0); if (resourceJid === APP.xmpp.myResource() && $('#contactlist>ul .title')[0].nextSibling.nextSibling) { clElement.insertBefore(newContact, $('#contactlist>ul .title')[0].nextSibling.nextSibling); } else { clElement.appendChild(newContact); } updateNumberOfParticipants(1); }, /** * Removes a contact for the given peer jid. * * @param peerJid the peerJid corresponding to the contact to remove */ removeContact: function (peerJid) { var resourceJid = Strophe.getResourceFromJid(peerJid); var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); if (contact && contact.length > 0) { var contactlist = $('#contactlist>ul'); contactlist.get(0).removeChild(contact.get(0)); updateNumberOfParticipants(-1); } }, setVisualNotification: function (show, stopGlowingIn) { var glower = $('#contactListButton'); if (show && !notificationInterval) { notificationInterval = window.setInterval(function () { glower.toggleClass('active glowing'); }, 800); } else if (!show && notificationInterval) { stopGlowing(glower); } if (stopGlowingIn) { setTimeout(function () { stopGlowing(glower); }, stopGlowingIn); } }, setClickable: function (resourceJid, isClickable) { var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); if (isClickable) { contact.addClass('clickable'); } else { contact.removeClass('clickable'); } }, onDisplayNameChange: function (peerJid, displayName) { if (peerJid === 'localVideoContainer') peerJid = APP.xmpp.myJid(); var resourceJid = Strophe.getResourceFromJid(peerJid); var contactName = $('#contactlist #' + resourceJid + '>p'); if (contactName && displayName && displayName.length > 0) contactName.html(displayName); } }; module.exports = ContactList; },{}],26:[function(require,module,exports){ var Avatar = require("../../avatar/Avatar"); var Settings = require("./../../../settings/Settings"); var UIUtil = require("../../util/UIUtil"); var languages = require("../../../../service/translation/languages"); function generateLanguagesSelectBox() { var currentLang = APP.translation.getCurrentLanguage(); var html = ""; } var SettingsMenu = { init: function () { $("#startMutedOptions").before(generateLanguagesSelectBox()); APP.translation.translateElement($("#languages_selectbox")); $('#settingsmenu>input').keyup(function(event){ if(event.keyCode === 13) {//enter SettingsMenu.update(); } }); if(APP.xmpp.isModerator()) { $("#startMutedOptions").css("display", "block"); } else { $("#startMutedOptions").css("display", "none"); } $("#updateSettings").click(function () { SettingsMenu.update(); }); }, onRoleChanged: function () { if(APP.xmpp.isModerator()) { $("#startMutedOptions").css("display", "block"); } else { $("#startMutedOptions").css("display", "none"); } }, setStartMuted: function (audio, video) { $("#startAudioMuted").attr("checked", audio); $("#startVideoMuted").attr("checked", video); }, 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() { 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); } } }; module.exports = SettingsMenu; },{"../../../../service/translation/languages":107,"../../avatar/Avatar":16,"../../util/UIUtil":33,"./../../../settings/Settings":43}],27:[function(require,module,exports){ var PanelToggler = require("../side_pannels/SidePanelToggler"); var buttonHandlers = { "bottom_toolbar_contact_list": function () { BottomToolbar.toggleContactList(); }, "bottom_toolbar_film_strip": function () { BottomToolbar.toggleFilmStrip(); }, "bottom_toolbar_chat": function () { BottomToolbar.toggleChat(); } }; var BottomToolbar = (function (my) { my.init = function () { for(var k in buttonHandlers) $("#" + k).click(buttonHandlers[k]); }; my.toggleChat = function() { PanelToggler.toggleChat(); }; my.toggleContactList = function() { PanelToggler.toggleContactList(); }; my.toggleFilmStrip = function() { var filmstrip = $("#remoteVideos"); filmstrip.toggleClass("hidden"); }; $(document).bind("remotevideo.resized", function (event, width, height) { var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18; $('#bottomToolbar').css({bottom: bottom + 'px'}); }); return my; }(BottomToolbar || {})); module.exports = BottomToolbar; },{"../side_pannels/SidePanelToggler":20}],28:[function(require,module,exports){ /* global APP,$, buttonClick, config, lockRoom, setSharedKey, Util */ 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 roomUrl = null; var sharedKey = ''; var UI = null; var buttonHandlers = { "toolbar_button_mute": function () { return APP.UI.toggleAudio(); }, "toolbar_button_camera": function () { return APP.UI.toggleVideo(); }, /*"toolbar_button_authentication": function () { return Toolbar.authenticateClicked(); },*/ "toolbar_button_record": function () { return toggleRecording(); }, "toolbar_button_security": function () { return Toolbar.openLockDialog(); }, "toolbar_button_link": function () { return Toolbar.openLinkDialog(); }, "toolbar_button_chat": function () { return BottomToolbar.toggleChat(); }, "toolbar_button_prezi": function () { return Prezi.openPreziDialog(); }, "toolbar_button_etherpad": function () { return Etherpad.toggleEtherpad(0); }, "toolbar_button_desktopsharing": function () { return APP.desktopsharing.toggleScreenSharing(); }, "toolbar_button_fullScreen": function() { UIUtil.buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen"); return Toolbar.toggleFullScreen(); }, "toolbar_button_sip": function () { return callSipButtonClicked(); }, "toolbar_button_dialpad": function () { return dialpadButtonClicked(); }, "toolbar_button_settings": function () { PanelToggler.toggleSettingsMenu(); }, "toolbar_button_hangup": function () { return hangup(); }, "toolbar_button_login": function () { Toolbar.authenticateClicked(); }, "toolbar_button_logout": function () { // Ask for confirmation messageHandler.openTwoButtonDialog( "dialog.logoutTitle", null, "dialog.logoutQuestion", null, false, "dialog.Yes", function (evt, yes) { if (yes) { APP.xmpp.logout(function (url) { if (url) { window.location.href = url; } else { hangup(); } }); } }); } }; function hangup() { APP.xmpp.disposeConference(); if(config.enableWelcomePage) { setTimeout(function() { window.localStorage.welcomePageDisabled = false; window.location.pathname = "/"; }, 10000); } var title = APP.translation.generateTranslatonHTML( "dialog.sessTerminated"); var msg = APP.translation.generateTranslatonHTML( "dialog.hungUp"); var button = APP.translation.generateTranslatonHTML( "dialog.joinAgain"); var buttons = []; buttons.push({title: button, value: true}); UI.messageHandler.openDialog( title, msg, true, buttons, function(event, value, message, formVals) { window.location.reload(); return false; } ); } /** * Starts or stops the recording for the conference. */ function toggleRecording() { APP.xmpp.toggleRecording(function (callback) { var msg = APP.translation.generateTranslatonHTML( "dialog.recordingToken"); var token = APP.translation.translateString("dialog.token"); APP.UI.messageHandler.openTwoButtonDialog(null, null, null, '

' + msg + '

' + '', 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, Toolbar.setRecordingButtonState); } /** * Locks / unlocks the room. */ function lockRoom(lock) { var currentSharedKey = ''; if (lock) currentSharedKey = sharedKey; APP.xmpp.lockRoom(currentSharedKey, function (res) { // password is required if (sharedKey) { console.log('set room password'); Toolbar.lockLockButton(); } else { console.log('removed room password'); Toolbar.unlockLockButton(); } }, function (err) { console.warn('setting password failed', err); messageHandler.showError("dialog.lockTitle", "dialog.lockMessage"); Toolbar.setSharedKey(''); }, function () { console.warn('room passwords not supported'); messageHandler.showError("dialog.warning", "dialog.passwordNotSupported"); Toolbar.setSharedKey(''); }); }; /** * Invite participants to conference. */ function inviteParticipants() { if (roomUrl === null) return; var sharedKeyText = ""; if (sharedKey && sharedKey.length > 0) { sharedKeyText = APP.translation.translateString("email.sharedKey", {sharedKey: sharedKey}); sharedKeyText = sharedKeyText.replace(/\n/g, "%0D%0A"); } var supportedBrowsers = "Chromium, Google Chrome " + APP.translation.translateString("email.and") + " Opera"; var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1); var subject = APP.translation.translateString("email.subject", {appName:interfaceConfig.APP_NAME, conferenceName: conferenceName}); var body = APP.translation.translateString("email.body", {appName:interfaceConfig.APP_NAME, sharedKeyText: sharedKeyText, roomUrl: roomUrl, supportedBrowsers: supportedBrowsers}); body = body.replace(/\n/g, "%0D%0A"); if (window.localStorage.displayname) { body += "%0D%0A%0D%0A" + window.localStorage.displayname; } if (interfaceConfig.INVITATION_POWERED_BY) { body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org"; } window.open("mailto:?subject=" + subject + "&body=" + body, '_blank'); } function dialpadButtonClicked() { //TODO show the dialpad window } function callSipButtonClicked() { var defaultNumber = config.defaultSipNumber ? config.defaultSipNumber : ''; var sipMsg = APP.translation.generateTranslatonHTML( "dialog.sipMsg"); messageHandler.openTwoButtonDialog(null, null, null, '

' + sipMsg + '

' + '', false, "dialog.Dial", function (e, v, m, f) { if (v) { var numberInput = f.sipNumber; if (numberInput) { APP.xmpp.dial( numberInput, 'fromnumber', UI.getRoomName(), sharedKey); } } }, null, null, ':input:first' ); } var Toolbar = (function (my) { my.init = function (ui) { 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); } } ); }, /** * 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.getMUCJoined()) { 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) { roomUrl = newRoomUrl; // If the invite dialog has been already opened we update the information. var 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 () { if (config.disablePrezi) { $("#prezi_button").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.generateTranslatonHTML( "dialog.passwordMsg"); var yourPassword = APP.translation.translateString( "dialog.yourPassword"); messageHandler.openTwoButtonDialog(null, null, null, '

' + msg + '

' + '', 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 inviteAttreibutes; if (roomUrl === null) { inviteAttreibutes = 'data-i18n="[value]roomUrlDefaultMsg" value="' + APP.translation.translateString("roomUrlDefaultMsg") + '"'; } else { inviteAttreibutes = "value=\"" + encodeURI(roomUrl) + "\""; } messageHandler.openTwoButtonDialog("dialog.shareLink", null, null, '', 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.generateTranslatonHTML( "dialog.settings1"); var settings2 = APP.translation.generateTranslatonHTML( "dialog.settings2"); var settings3 = APP.translation.generateTranslatonHTML( "dialog.settings3"); var yourPassword = APP.translation.translateString( "dialog.yourPassword"); messageHandler.openTwoButtonDialog(null, '

' + settings1 + '

' + '' + settings2 + '
' + '' + settings3 + '', 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 () { if ($("#lockIcon").hasClass("icon-security-locked")) UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked"); }; /** * Updates the lock button state to locked. */ my.lockLockButton = function () { if ($("#lockIcon").hasClass("icon-security")) UIUtil.buttonClick("#lockIcon", "icon-security icon-security-locked"); }; /** * Shows or hides authentication button * @param show true to show or false to hide */ my.showAuthenticateButton = function (show) { if (show) { $('#authentication').css({display: "inline"}); } else { $('#authentication').css({display: "none"}); } }; // Shows or hides the 'recording' button. my.showRecordingButton = function (show) { if (!config.enableRecording) { return; } if (show) { $('#recording').css({display: "inline"}); } else { $('#recording').css({display: "none"}); } }; // Sets the state of the recording button my.setRecordingButtonState = function (isRecording) { var selector = $('#recordButton'); if (isRecording) { selector.removeClass("icon-recEnable"); selector.addClass("icon-recEnable active"); } else { selector.removeClass("icon-recEnable active"); selector.addClass("icon-recEnable"); } }; // Shows or hides SIP calls button my.showSipCallButton = function (show) { if (APP.xmpp.isSipGatewayEnabled() && show) { $('#sipCallButton').css({display: "inline-block"}); } else { $('#sipCallButton').css({display: "none"}); } }; // Shows or hides the dialpad button my.showDialPadButton = function (show) { if (show) { $('#dialPadButton').css({display: "inline-block"}); } else { $('#dialPadButton').css({display: "none"}); } }; /** * Displays user authenticated identity name(login). * @param authIdentity identity name to be displayed. */ my.setAuthenticatedIdentity = function (authIdentity) { if (authIdentity) { var 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 true to show */ my.showLoginButton = function (show) { if (show) { $('#toolbar_button_login').css({display: "list-item"}); } else { $('#toolbar_button_login').css({display: "none"}); } }; /** * Shows/hides logout button. * @param show true to show */ my.showLogoutButton = function (show) { if (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 = $("#desktopsharing > a"); if (active) { button.addClass("glow"); } else { button.removeClass("glow"); } }; return my; }(Toolbar || {})); module.exports = Toolbar; },{"../../../service/authentication/AuthenticationEvents":103,"../authentication/Authentication":14,"../etherpad/Etherpad":17,"../prezi/Prezi":18,"../side_pannels/SidePanelToggler":20,"../util/MessageHandler":31,"../util/UIUtil":33,"./BottomToolbar":27}],29:[function(require,module,exports){ /* global $, interfaceConfig, Moderator, DesktopStreaming.showDesktopSharingButton */ var toolbarTimeoutObject, toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT; function showDesktopSharingButton() { if (APP.desktopsharing.isDesktopSharingEnabled()) { $('#desktopsharing').css({display: "inline"}); } else { $('#desktopsharing').css({display: "none"}); } } /** * Hides the toolbar. */ function hideToolbar() { if(config.alwaysVisibleToolbar) return; var header = $("#header"), bottomToolbar = $("#bottomToolbar"); var isToolbarHover = false; header.find('*').each(function () { var id = $(this).attr('id'); if ($("#" + id + ":hover").length > 0) { isToolbarHover = true; } }); if ($("#bottomToolbar:hover").length > 0) { isToolbarHover = true; } clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; if (!isToolbarHover) { header.hide("slide", { direction: "up", duration: 300}); $('#subject').animate({top: "-=40"}, 300); if ($("#remoteVideos").hasClass("hidden")) { bottomToolbar.hide( "slide", {direction: "right", duration: 300}); } } else { toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); } } var ToolbarToggler = { /** * Shows the main toolbar. */ showToolbar: function () { var header = $("#header"), 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}); } if (toolbarTimeoutObject) { clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; } toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); 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(); }, /** * Docks/undocks the toolbar. * * @param isDock indicates what operation to perform */ dockToolbar: function (isDock) { if (isDock) { // First make sure the toolbar is shown. if (!$('#header').is(':visible')) { this.showToolbar(); } // Then clear the time out, to dock the toolbar. if (toolbarTimeoutObject) { clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; } } else { if (!$('#header').is(':visible')) { this.showToolbar(); } else { toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); } } }, showDesktopSharingButton: showDesktopSharingButton }; module.exports = ToolbarToggler; },{}],30:[function(require,module,exports){ var JitsiPopover = (function () { /** * Constructs new JitsiPopover and attaches it to the element * @param element jquery selector * @param options the options for the popover. * @constructor */ function JitsiPopover(element, options) { this.options = { skin: "white", content: "" }; if(options) { if(options.skin) this.options.skin = options.skin; if(options.content) this.options.content = options.content; } this.elementIsHovered = false; this.popoverIsHovered = false; this.popoverShown = false; element.data("jitsi_popover", this); this.element = element; this.template = '
' + '
'; var self = this; this.element.on("mouseenter", function () { self.elementIsHovered = true; self.show(); }).on("mouseleave", function () { self.elementIsHovered = false; setTimeout(function () { self.hide(); }, 10); }); } /** * Shows the popover */ JitsiPopover.prototype.show = function () { this.createPopover(); this.popoverShown = true; }; /** * Hides the popover */ JitsiPopover.prototype.hide = function () { if(!this.elementIsHovered && !this.popoverIsHovered && this.popoverShown) { this.forceHide(); } }; /** * Hides the popover */ JitsiPopover.prototype.forceHide = function () { $(".jitsipopover").remove(); this.popoverShown = false; }; /** * Creates the popover html */ JitsiPopover.prototype.createPopover = function () { $("body").append(this.template); $(".jitsipopover > .jitsipopover-content").html(this.options.content); var self = this; $(".jitsipopover").on("mouseenter", function () { self.popoverIsHovered = true; }).on("mouseleave", function () { self.popoverIsHovered = false; self.hide(); }); this.refreshPosition(); }; /** * Refreshes the position of the popover */ JitsiPopover.prototype.refreshPosition = function () { $(".jitsipopover").position({ my: "bottom", at: "top", collision: "fit", of: this.element, using: function (position, elements) { var calcLeft = elements.target.left - elements.element.left + elements.target.width/2; $(".jitsipopover").css({top: position.top, left: position.left, display: "table"}); $(".jitsipopover > .arrow").css({left: calcLeft}); $(".jitsipopover > .jitsiPopupmenuPadding").css({left: calcLeft - 50}); } }); }; /** * Updates the content of popover. * @param content new content */ JitsiPopover.prototype.updateContent = function (content) { this.options.content = content; if(!this.popoverShown) return; $(".jitsipopover").remove(); this.createPopover(); }; return JitsiPopover; })(); module.exports = JitsiPopover; },{}],31:[function(require,module,exports){ /* global $, APP, jQuery, toastr */ var messageHandler = (function(my) { /** * Shows a message to the user. * * @param titleString the title of the message * @param messageString the text of the message */ my.openMessageDialog = function(titleKey, messageKey) { var title = null; if(titleKey) { title = APP.translation.generateTranslatonHTML(titleKey); } var message = APP.translation.generateTranslatonHTML(messageKey); $.prompt(message, { title: title, persistent: false } ); }; /** * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel. * * @param titleString the title of the message * @param msgString the text of the message * @param persistent boolean value which determines whether the message is persistent or not * @param leftButton the fist button's text * @param submitFunction function to be called on submit * @param loadedFunction function to be called after the prompt is fully loaded * @param closeFunction function to be called after the prompt is closed * @param focus optional focus selector or button index to be focused after * the dialog is opened * @param defaultButton index of default button which will be activated when * the user press 'enter'. Indexed from 0. */ my.openTwoButtonDialog = function(titleKey, titleString, msgKey, msgString, persistent, leftButtonKey, submitFunction, loadedFunction, closeFunction, focus, defaultButton) { var buttons = []; var leftButton = APP.translation.generateTranslatonHTML(leftButtonKey); buttons.push({ title: leftButton, value: true}); var cancelButton = APP.translation.generateTranslatonHTML("dialog.Cancel"); buttons.push({title: cancelButton, value: false}); var message = msgString, title = titleString; if (titleKey) { title = APP.translation.generateTranslatonHTML(titleKey); } if (msgKey) { message = APP.translation.generateTranslatonHTML(msgKey); } $.prompt(message, { title: title, persistent: false, buttons: buttons, defaultButton: defaultButton, focus: focus, loaded: loadedFunction, submit: submitFunction, close: closeFunction }); }; /** * Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel. * * @param titleString the title of the message * @param msgString the text of the message * @param persistent boolean value which determines whether the message is persistent or not * @param buttons object with the buttons. The keys must be the name of the button and value is the value * that will be passed to submitFunction * @param submitFunction function to be called on submit * @param loadedFunction function to be called after the prompt is fully loaded */ my.openDialog = function (titleString, msgString, persistent, buttons, submitFunction, loadedFunction) { var args = { title: titleString, persistent: persistent, buttons: buttons, defaultButton: 1, loaded: loadedFunction, submit: submitFunction }; if (persistent) { args.closeText = ''; } return new Impromptu(msgString, args); }; /** * Closes currently opened dialog. */ my.closeDialog = function () { $.prompt.close(); }; /** * Shows a dialog with different states to the user. * * @param statesObject object containing all the states of the dialog */ my.openDialogWithStates = function (statesObject, options) { return new Impromptu(statesObject, options); }; /** * Opens new popup window for given url centered over current * window. * * @param url the URL to be displayed in the popup window * @param w the width of the popup window * @param h the height of the popup window * @param onPopupClosed optional callback function called when popup window * has been closed. * * @returns popup window object if opened successfully or undefined * in case we failed to open it(popup blocked) */ my.openCenteredPopup = function (url, w, h, onPopupClosed) { var l = window.screenX + (window.innerWidth / 2) - (w / 2); var t = window.screenY + (window.innerHeight / 2) - (h / 2); var popup = window.open( url, '_blank', 'top=' + t + ', left=' + l + ', width=' + w + ', height=' + h + ''); if (popup && onPopupClosed) { var pollTimer = window.setInterval(function () { if (popup.closed !== false) { window.clearInterval(pollTimer); onPopupClosed(); } }, 200); } return popup; }; /** * Shows a dialog prompting the user to send an error report. * * @param titleString the title of the message * @param msgString the text of the message * @param error the error that is being reported */ my.openReportDialog = function(titleKey, msgKey, error) { my.openMessageDialog(titleKey, msgKey); console.log(error); //FIXME send the error to the server }; /** * Shows an error dialog to the user. * @param title the title of the message * @param message the text of the messafe */ my.showError = function(titleKey, msgKey) { if(!titleKey) { titleKey = "dialog.oops"; } if(!msgKey) { msgKey = "dialog.defaultError"; } messageHandler.openMessageDialog(titleKey, msgKey); }; my.notify = function(displayName, displayNameKey, cls, messageKey, messageArguments, options) { var displayNameSpan = '" + APP.translation.translateString(displayNameKey); } displayNameSpan += ""; toastr.info( displayNameSpan + '
' + '" + APP.translation.translateString(messageKey, messageArguments) + '', null, options); }; return my; }(messageHandler || {})); module.exports = messageHandler; },{}],32:[function(require,module,exports){ var UIEvents = require("../../../service/UI/UIEvents"); var nickname = null; var eventEmitter = null; var NickanameHandler = { 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 = NickanameHandler; },{"../../../service/UI/UIEvents":102}],33:[function(require,module,exports){ /** * Created by hristo on 12/22/14. */ module.exports = { /** * Returns the available video width. */ getAvailableVideoWidth: function () { var PanelToggler = require("../side_pannels/SidePanelToggler"); var rightPanelWidth = PanelToggler.isVisible() ? PanelToggler.getPanelSize()[0] : 0; return window.innerWidth - rightPanelWidth; }, /** * Changes the style class of the element given by id. */ buttonClick: function(id, classname) { $(id).toggleClass(classname); // add the class to the clicked element }, /** * Returns the text width for the given element. * * @param el the element */ getTextWidth: function (el) { return (el.clientWidth + 1); }, /** * Returns the text height for the given element. * * @param el the element */ getTextHeight: function (el) { return (el.clientHeight + 1); }, /** * Plays the sound given by id. * * @param id the identifier of the audio element. */ playSoundNotification: function (id) { document.getElementById(id).play(); }, /** * Escapes the given text. */ escapeHtml: function (unsafeText) { return $('
').text(unsafeText).html(); }, imageToGrayScale: function (canvas) { var context = canvas.getContext('2d'); var imgData = context.getImageData(0, 0, canvas.width, canvas.height); var pixels = imgData.data; for (var i = 0, n = pixels.length; i < n; i += 4) { var grayscale = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11; pixels[i ] = grayscale; // red pixels[i+1] = grayscale; // green pixels[i+2] = grayscale; // blue // pixels[i+3] is alpha } // redraw the image in black & white context.putImageData(imgData, 0, 0); }, setTooltip: function (element, key, position) { element.setAttribute("data-i18n", "[data-content]" + key); element.setAttribute("data-toggle", "popover"); element.setAttribute("data-placement", position); element.setAttribute("data-html", true); element.setAttribute("data-container", "body"); } }; },{"../side_pannels/SidePanelToggler":20}],34:[function(require,module,exports){ var JitsiPopover = require("../util/JitsiPopover"); /** * Constructs new connection indicator. * @param videoContainer the video container associated with the indicator. * @constructor */ function ConnectionIndicator(videoContainer, jid, VideoLayout) { this.videoContainer = videoContainer; this.bandwidth = null; this.packetLoss = null; this.bitrate = null; this.showMoreValue = false; this.resolution = null; this.transport = []; this.popover = null; this.jid = jid; this.create(); this.videoLayout = VideoLayout; } /** * Values for the connection quality * @type {{98: string, * 81: string, * 64: string, * 47: string, * 30: string, * 0: string}} */ ConnectionIndicator.connectionQualityValues = { 98: "18px", //full 81: "15px",//4 bars 64: "11px",//3 bars 47: "7px",//2 bars 30: "3px",//1 bar 0: "0px"//empty }; ConnectionIndicator.getIP = function(value) { return value.substring(0, value.lastIndexOf(":")); }; ConnectionIndicator.getPort = function(value) { return value.substring(value.lastIndexOf(":") + 1, value.length); }; ConnectionIndicator.getStringFromArray = function (array) { var res = ""; for(var i = 0; i < array.length; i++) { res += (i === 0? "" : ", ") + array[i]; } return res; }; /** * Generates the html content. * @returns {string} the html content. */ ConnectionIndicator.prototype.generateText = function () { var downloadBitrate, uploadBitrate, packetLoss, resolution, i; var translate = APP.translation.translateString; if(this.bitrate === null) { downloadBitrate = "N/A"; uploadBitrate = "N/A"; } else { downloadBitrate = this.bitrate.download? this.bitrate.download + " Kbps" : "N/A"; uploadBitrate = this.bitrate.upload? this.bitrate.upload + " Kbps" : "N/A"; } if(this.packetLoss === null) { packetLoss = "N/A"; } else { packetLoss = "" + (this.packetLoss.download !== null? this.packetLoss.download : "N/A") + "% " + (this.packetLoss.upload !== null? this.packetLoss.upload : "N/A") + "%"; } var resolutionValue = null; if(this.resolution && this.jid != null) { var keys = Object.keys(this.resolution); for(var ssrc in this.resolution) { resolutionValue = this.resolution[ssrc]; } } if(this.jid === null) { resolution = ""; if(this.resolution === null || !Object.keys(this.resolution) || Object.keys(this.resolution).length === 0) { resolution = "N/A"; } else for(i in this.resolution) { resolutionValue = this.resolution[i]; if(resolutionValue) { if(resolutionValue.height && resolutionValue.width) { resolution += (resolution === ""? "" : ", ") + resolutionValue.width + "x" + resolutionValue.height; } } } } else if(!resolutionValue || !resolutionValue.height || !resolutionValue.width) { resolution = "N/A"; } else { resolution = resolutionValue.width + "x" + resolutionValue.height; } var result = "" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + translate("connectionindicator.bitrate") + "" + downloadBitrate + " " + uploadBitrate + "
" + translate("connectionindicator.packetloss") + "" + packetLoss + "
" + translate("connectionindicator.resolution") + "" + resolution + "
"; if(this.videoContainer.id == "localVideoContainer") { result += "
" + translate("connectionindicator." + (this.showMoreValue ? "less" : "more")) + "

"; } if(this.showMoreValue) { var downloadBandwidth, uploadBandwidth, transport; if(this.bandwidth === null) { downloadBandwidth = "N/A"; uploadBandwidth = "N/A"; } else { downloadBandwidth = this.bandwidth.download? this.bandwidth.download + " Kbps" : "N/A"; uploadBandwidth = this.bandwidth.upload? this.bandwidth.upload + " Kbps" : "N/A"; } if(!this.transport || this.transport.length === 0) { transport = "" + "" + translate("connectionindicator.address") + "" + " N/A"; } else { var data = {remoteIP: [], localIP:[], remotePort:[], localPort:[]}; for(i = 0; i < this.transport.length; i++) { var ip = ConnectionIndicator.getIP(this.transport[i].ip); var port = ConnectionIndicator.getPort(this.transport[i].ip); var localIP = ConnectionIndicator.getIP(this.transport[i].localip); var localPort = ConnectionIndicator.getPort(this.transport[i].localip); if(data.remoteIP.indexOf(ip) == -1) { data.remoteIP.push(ip); } if(data.remotePort.indexOf(port) == -1) { data.remotePort.push(port); } if(data.localIP.indexOf(localIP) == -1) { data.localIP.push(localIP); } if(data.localPort.indexOf(localPort) == -1) { data.localPort.push(localPort); } } var local_address_key = "connectionindicator.localaddress"; var remote_address_key = "connectionindicator.remoteaddress"; var localTransport = "" + translate(local_address_key, {count: data.localIP.length}) + " " + ConnectionIndicator.getStringFromArray(data.localIP) + ""; transport = "" + translate(remote_address_key, {count: data.remoteIP.length}) + " " + ConnectionIndicator.getStringFromArray(data.remoteIP) + ""; var key_remote = "connectionindicator.remoteport", key_local = "connectionindicator.localport"; transport += "" + "" + "" + translate(key_remote, {count: this.transport.length}) + ""; localTransport += "" + "" + "" + translate(key_local, {count: this.transport.length}) + ""; transport += ConnectionIndicator.getStringFromArray(data.remotePort); localTransport += ConnectionIndicator.getStringFromArray(data.localPort); transport += ""; transport += localTransport + ""; transport +="" + "" + translate("connectionindicator.transport") + "" + "" + this.transport[0].type + ""; } result += "" + "" + ""; result += transport + "
" + "" + translate("connectionindicator.bandwidth") + "" + "" + "" + downloadBandwidth + " " + uploadBandwidth + "
"; } return result; }; /** * Shows or hide the additional information. */ ConnectionIndicator.prototype.showMore = function () { this.showMoreValue = !this.showMoreValue; this.updatePopoverData(); }; function createIcon(classes) { var icon = document.createElement("span"); for(var i in classes) { icon.classList.add(classes[i]); } icon.appendChild( document.createElement("i")).classList.add("icon-connection"); return icon; } /** * Creates the indicator */ ConnectionIndicator.prototype.create = function () { this.connectionIndicatorContainer = document.createElement("div"); this.connectionIndicatorContainer.className = "connectionindicator"; this.connectionIndicatorContainer.style.display = "none"; this.videoContainer.appendChild(this.connectionIndicatorContainer); this.popover = new JitsiPopover( $("#" + this.videoContainer.id + " > .connectionindicator"), {content: "
" + APP.translation.translateString("connectionindicator.na") + "
", skin: "black"}); this.emptyIcon = this.connectionIndicatorContainer.appendChild( createIcon(["connection", "connection_empty"])); this.fullIcon = this.connectionIndicatorContainer.appendChild( createIcon(["connection", "connection_full"])); }; /** * Removes the indicator */ ConnectionIndicator.prototype.remove = function() { this.connectionIndicatorContainer.remove(); this.popover.forceHide(); }; /** * Updates the data of the indicator * @param percent the percent of connection quality * @param object the statistics data. */ ConnectionIndicator.prototype.updateConnectionQuality = function (percent, object) { if(percent === null) { this.connectionIndicatorContainer.style.display = "none"; this.popover.forceHide(); return; } else { if(this.connectionIndicatorContainer.style.display == "none") { this.connectionIndicatorContainer.style.display = "block"; this.videoLayout.updateMutePosition(this.videoContainer.id); } } this.bandwidth = object.bandwidth; this.bitrate = object.bitrate; this.packetLoss = object.packetLoss; this.transport = object.transport; if(object.resolution) { this.resolution = object.resolution; } for(var quality in ConnectionIndicator.connectionQualityValues) { if(percent >= quality) { this.fullIcon.style.width = ConnectionIndicator.connectionQualityValues[quality]; } } this.updatePopoverData(); }; /** * Updates the resolution * @param resolution the new resolution */ ConnectionIndicator.prototype.updateResolution = function (resolution) { this.resolution = resolution; this.updatePopoverData(); }; /** * Updates the content of the popover */ ConnectionIndicator.prototype.updatePopoverData = function () { this.popover.updateContent( "
" + this.generateText() + "
"); APP.translation.translateElement($(".connection_info")); }; /** * Hides the popover */ ConnectionIndicator.prototype.hide = function () { this.popover.forceHide(); }; /** * Hides the indicator */ ConnectionIndicator.prototype.hideIndicator = function () { this.connectionIndicatorContainer.style.display = "none"; if(this.popover) this.popover.forceHide(); }; module.exports = ConnectionIndicator; },{"../util/JitsiPopover":30}],35:[function(require,module,exports){ var AudioLevels = require("../audio_levels/AudioLevels"); var Avatar = require("../avatar/Avatar"); var Chat = require("../side_pannels/chat/Chat"); var ContactList = require("../side_pannels/contactlist/ContactList"); var UIUtil = require("../util/UIUtil"); var ConnectionIndicator = require("./ConnectionIndicator"); var NicknameHandler = require("../util/NicknameHandler"); var MediaStreamType = require("../../../service/RTC/MediaStreamTypes"); var UIEvents = require("../../../service/UI/UIEvents"); var currentDominantSpeaker = null; var lastNCount = config.channelLastN; var localLastNCount = config.channelLastN; var localLastNSet = []; var lastNEndpointsCache = []; var lastNPickupJid = null; var largeVideoState = { updateInProgress: false, newSrc: '' }; var eventEmitter = null; /** * Currently focused video "src"(displayed in large video). * @type {String} */ var focusedVideoInfo = null; var mutedAudios = {}; var flipXLocalVideo = true; var currentVideoWidth = null; var currentVideoHeight = null; var localVideoSrc = null; function videoactive( videoelem) { if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { // ignore mixedmslabela0 and v0 videoelem.show(); VideoLayout.resizeThumbnails(); var videoParent = videoelem.parent(); var parentResourceJid = null; if (videoParent) parentResourceJid = VideoLayout.getPeerContainerResourceJid(videoParent[0]); // Update the large video to the last added video only if there's no // current dominant, focused speaker or prezi playing or update it to // the current dominant speaker. if ((!focusedVideoInfo && !VideoLayout.getDominantSpeakerResourceJid() && !require("../prezi/Prezi").isPresentationVisible()) || (parentResourceJid && VideoLayout.getDominantSpeakerResourceJid() === parentResourceJid)) { VideoLayout.updateLargeVideo( APP.RTC.getVideoSrc(videoelem[0]), 1, parentResourceJid); } VideoLayout.showModeratorIndicator(); } } function waitForRemoteVideo(selector, ssrc, stream, jid) { // XXX(gp) so, every call to this function is *always* preceded by a call // to the RTC.attachMediaStream() function but that call is *not* followed // by an update to the videoSrcToSsrc map! // // The above way of doing things results in video SRCs that don't correspond // to any SSRC for a short period of time (to be more precise, for as long // the waitForRemoteVideo takes to complete). This causes problems (see // bellow). // // I'm wondering why we need to do that; i.e. why call RTC.attachMediaStream() // a second time in here and only then update the videoSrcToSsrc map? Why // not simply update the videoSrcToSsrc map when the RTC.attachMediaStream() // is called the first time? I actually do that in the lastN changed event // handler because the "orphan" video SRC is causing troubles there. The // purpose of this method would then be to fire the "videoactive.jingle". // // Food for though I guess :-) if (selector.removed || !selector.parent().is(":visible")) { console.warn("Media removed before had started", selector); return; } if (stream.id === 'mixedmslabel') return; if (selector[0].currentTime > 0) { APP.RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF? videoactive(selector); } else { setTimeout(function () { waitForRemoteVideo(selector, ssrc, stream, jid); }, 250); } } /** * Returns an array of the video horizontal and vertical indents, * so that if fits its parent. * * @return an array with 2 elements, the horizontal indent and the vertical * indent */ function getCameraVideoPosition(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight) { // Parent height isn't completely calculated when we position the video in // full screen mode and this is why we use the screen height in this case. // Need to think it further at some point and implement it properly. var isFullScreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen; if (isFullScreen) videoSpaceHeight = window.innerHeight; var horizontalIndent = (videoSpaceWidth - videoWidth) / 2; var verticalIndent = (videoSpaceHeight - videoHeight) / 2; return [horizontalIndent, verticalIndent]; } /** * Returns an array of the video horizontal and vertical indents. * Centers horizontally and top aligns vertically. * * @return an array with 2 elements, the horizontal indent and the vertical * indent */ function getDesktopVideoPosition(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight) { var horizontalIndent = (videoSpaceWidth - videoWidth) / 2; var verticalIndent = 0;// Top aligned return [horizontalIndent, verticalIndent]; } /** * Returns an array of the video dimensions, so that it covers the screen. * It leaves no empty areas, but some parts of the video might not be visible. * * @return an array with 2 elements, the video width and the video height */ function getCameraVideoSize(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight) { if (!videoWidth) videoWidth = currentVideoWidth; if (!videoHeight) videoHeight = currentVideoHeight; var aspectRatio = videoWidth / videoHeight; var availableWidth = Math.max(videoWidth, videoSpaceWidth); var availableHeight = Math.max(videoHeight, videoSpaceHeight); if (availableWidth / aspectRatio < videoSpaceHeight) { availableHeight = videoSpaceHeight; availableWidth = availableHeight * aspectRatio; } if (availableHeight * aspectRatio < videoSpaceWidth) { availableWidth = videoSpaceWidth; availableHeight = availableWidth / aspectRatio; } return [availableWidth, availableHeight]; } /** * Sets the display name for the given video span id. */ function setDisplayName(videoSpanId, displayName, key) { if (!$('#' + videoSpanId).length) { console.warn( "Unable to set displayName - " + videoSpanId + " does not exist"); return; } var nameSpan = $('#' + videoSpanId + '>span.displayname'); var defaultLocalDisplayName = APP.translation.generateTranslatonHTML( interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME); // If we already have a display name for this video. if (nameSpan.length > 0) { var nameSpanElement = nameSpan.get(0); if (nameSpanElement.id === 'localDisplayName' && $('#localDisplayName').text() !== displayName) { if (displayName && displayName.length > 0) { var meHTML = APP.translation.generateTranslatonHTML("me"); $('#localDisplayName').html(displayName + ' (' + meHTML + ')'); } else $('#localDisplayName').html(defaultLocalDisplayName); } else { if (displayName && displayName.length > 0) { $('#' + videoSpanId + '_name').html(displayName); } else if (key && key.length > 0) { var nameHtml = APP.translation.generateTranslatonHTML(key); $('#' + videoSpanId + '_name').html(nameHtml); } else $('#' + videoSpanId + '_name').text( interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME); } } else { var editButton = null; nameSpan = document.createElement('span'); nameSpan.className = 'displayname'; $('#' + videoSpanId)[0].appendChild(nameSpan); if (videoSpanId === 'localVideoContainer') { editButton = createEditDisplayNameButton(); if (displayName && displayName.length > 0) { var meHTML = APP.translation.generateTranslatonHTML("me"); nameSpan.innerHTML = displayName + meHTML; } else nameSpan.innerHTML = defaultLocalDisplayName; } else { if (displayName && displayName.length > 0) { nameSpan.innerText = displayName; } else nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; } if (!editButton) { nameSpan.id = videoSpanId + '_name'; } else { nameSpan.id = 'localDisplayName'; $('#' + videoSpanId)[0].appendChild(editButton); //translates popover of edit button APP.translation.translateElement($("a.displayname")); var editableText = document.createElement('input'); editableText.className = 'displayname'; editableText.type = 'text'; editableText.id = 'editDisplayName'; if (displayName && displayName.length) { editableText.value = displayName; } var defaultNickname = APP.translation.translateString( "defaultNickname", {name: "Jane Pink"}); editableText.setAttribute('style', 'display:none;'); editableText.setAttribute('data-18n', '[placeholder]defaultNickname'); editableText.setAttribute("data-i18n-options", JSON.stringify({name: "Jane Pink"})); editableText.setAttribute("placeholder", defaultNickname); $('#' + videoSpanId)[0].appendChild(editableText); $('#localVideoContainer .displayname') .bind("click", function (e) { e.preventDefault(); e.stopPropagation(); $('#localDisplayName').hide(); $('#editDisplayName').show(); $('#editDisplayName').focus(); $('#editDisplayName').select(); $('#editDisplayName').one("focusout", function (e) { VideoLayout.inputDisplayNameHandler(this.value); }); $('#editDisplayName').on('keydown', function (e) { if (e.keyCode === 13) { e.preventDefault(); VideoLayout.inputDisplayNameHandler(this.value); } }); }); } } } /** * Gets the selector of video thumbnail container for the user identified by * given userJid * @param resourceJid user's Jid for whom we want to get the video container. */ function getParticipantContainer(resourceJid) { if (!resourceJid) return null; if (resourceJid === APP.xmpp.myResource()) return $("#localVideoContainer"); else return $("#participant_" + resourceJid); } /** * Sets the size and position of the given video element. * * @param video the video element to position * @param width the desired video width * @param height the desired video height * @param horizontalIndent the left and right indent * @param verticalIndent the top and bottom indent */ function positionVideo(video, width, height, horizontalIndent, verticalIndent) { video.width(width); video.height(height); video.css({ top: verticalIndent + 'px', bottom: verticalIndent + 'px', left: horizontalIndent + 'px', right: horizontalIndent + 'px'}); } /** * Adds the remote video menu element for the given jid in the * given parentElement. * * @param jid the jid indicating the video for which we're adding a menu. * @param parentElement the parent element where this menu will be added */ function addRemoteVideoMenu(jid, parentElement) { var spanElement = document.createElement('span'); spanElement.className = 'remotevideomenu'; parentElement.appendChild(spanElement); var menuElement = document.createElement('i'); menuElement.className = 'fa fa-angle-down'; menuElement.title = 'Remote user controls'; spanElement.appendChild(menuElement); // var popupmenuElement = document.createElement('ul'); popupmenuElement.className = 'popupmenu'; popupmenuElement.id = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid); spanElement.appendChild(popupmenuElement); var muteMenuItem = document.createElement('li'); var muteLinkItem = document.createElement('a'); var mutedIndicator = ""; if (!mutedAudios[jid]) { muteLinkItem.innerHTML = mutedIndicator + "
"; muteLinkItem.className = 'mutelink'; } else { muteLinkItem.innerHTML = mutedIndicator + "
"; muteLinkItem.className = 'mutelink disabled'; } muteLinkItem.onclick = function(){ if ($(this).attr('disabled') != undefined) { event.preventDefault(); } var isMute = mutedAudios[jid] == true; APP.xmpp.setMute(jid, !isMute); popupmenuElement.setAttribute('style', 'display:none;'); if (isMute) { this.innerHTML = mutedIndicator + "
"; this.className = 'mutelink disabled'; } else { this.innerHTML = mutedIndicator + "
"; this.className = 'mutelink'; } }; muteMenuItem.appendChild(muteLinkItem); popupmenuElement.appendChild(muteMenuItem); var ejectIndicator = ""; var ejectMenuItem = document.createElement('li'); var ejectLinkItem = document.createElement('a'); var ejectText = "
 
"; ejectLinkItem.innerHTML = ejectIndicator + ' ' + ejectText; ejectLinkItem.onclick = function(){ APP.xmpp.eject(jid); popupmenuElement.setAttribute('style', 'display:none;'); }; ejectMenuItem.appendChild(ejectLinkItem); popupmenuElement.appendChild(ejectMenuItem); var paddingSpan = document.createElement('span'); paddingSpan.className = 'popupmenuPadding'; popupmenuElement.appendChild(paddingSpan); APP.translation.translateElement($("#" + popupmenuElement.id + " > li > a > div")); } /** * Removes remote video menu element from video element identified by * given videoElementId. * * @param videoElementId the id of local or remote video element. */ function removeRemoteVideoMenu(videoElementId) { var menuSpan = $('#' + videoElementId + '>span.remotevideomenu'); if (menuSpan.length) { menuSpan.remove(); } } /** * Updates the data for the indicator * @param id the id of the indicator * @param percent the percent for connection quality * @param object the data */ function updateStatsIndicator(id, percent, object) { if(VideoLayout.connectionIndicators[id]) VideoLayout.connectionIndicators[id].updateConnectionQuality(percent, object); } /** * Returns an array of the video dimensions, so that it keeps it's aspect * ratio and fits available area with it's larger dimension. This method * ensures that whole video will be visible and can leave empty areas. * * @return an array with 2 elements, the video width and the video height */ function getDesktopVideoSize(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight) { if (!videoWidth) videoWidth = currentVideoWidth; if (!videoHeight) videoHeight = currentVideoHeight; var aspectRatio = videoWidth / videoHeight; var availableWidth = Math.max(videoWidth, videoSpaceWidth); var availableHeight = Math.max(videoHeight, videoSpaceHeight); videoSpaceHeight -= $('#remoteVideos').outerHeight(); if (availableWidth / aspectRatio >= videoSpaceHeight) { availableHeight = videoSpaceHeight; availableWidth = availableHeight * aspectRatio; } if (availableHeight * aspectRatio >= videoSpaceWidth) { availableWidth = videoSpaceWidth; availableHeight = availableWidth / aspectRatio; } return [availableWidth, availableHeight]; } /** * Creates the edit display name button. * * @returns the edit button */ function createEditDisplayNameButton() { var editButton = document.createElement('a'); editButton.className = 'displayname'; UIUtil.setTooltip(editButton, "videothumbnail.editnickname", "top"); editButton.innerHTML = ''; return editButton; } /** * Creates the element indicating the moderator(owner) of the conference. * * @param parentElement the parent element where the owner indicator will * be added */ function createModeratorIndicatorElement(parentElement) { var moderatorIndicator = document.createElement('i'); moderatorIndicator.className = 'fa fa-star'; parentElement.appendChild(moderatorIndicator); UIUtil.setTooltip(parentElement, "videothumbnail.moderator", "top"); } var VideoLayout = (function (my) { my.connectionIndicators = {}; // By default we use camera my.getVideoSize = getCameraVideoSize; my.getVideoPosition = getCameraVideoPosition; my.init = function (emitter) { // Listen for large video size updates document.getElementById('largeVideo') .addEventListener('loadedmetadata', function (e) { currentVideoWidth = this.videoWidth; currentVideoHeight = this.videoHeight; VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight); }); eventEmitter = emitter; }; my.isInLastN = function(resource) { return lastNCount < 0 // lastN is disabled, return true || (lastNCount > 0 && lastNEndpointsCache.length == 0) // lastNEndpoints cache not built yet, return true || (lastNEndpointsCache && lastNEndpointsCache.indexOf(resource) !== -1); }; my.changeLocalStream = function (stream, isMuted) { VideoLayout.changeLocalVideo(stream, isMuted); }; my.changeLocalAudio = function(stream, isMuted) { if(isMuted) APP.UI.setAudioMuted(true, true); APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream()); document.getElementById('localAudio').autoplay = true; document.getElementById('localAudio').volume = 0; }; my.changeLocalVideo = function(stream, isMuted) { // Set default display name. setDisplayName('localVideoContainer'); if(!VideoLayout.connectionIndicators["localVideoContainer"]) { VideoLayout.connectionIndicators["localVideoContainer"] = new ConnectionIndicator($("#localVideoContainer")[0], null, VideoLayout); } AudioLevels.updateAudioLevelCanvas(null, VideoLayout); var localVideo = null; function localVideoClick(event) { event.stopPropagation(); VideoLayout.handleVideoThumbClicked( APP.RTC.getVideoSrc(localVideo), false, APP.xmpp.myResource()); } $('#localVideoContainer').click(localVideoClick); // Add hover handler $('#localVideoContainer').hover( function() { VideoLayout.showDisplayName('localVideoContainer', true); }, function() { if (!VideoLayout.isLargeVideoVisible() || APP.RTC.getVideoSrc(localVideo) !== APP.RTC.getVideoSrc($('#largeVideo')[0])) VideoLayout.showDisplayName('localVideoContainer', false); } ); if(isMuted) { APP.UI.setVideoMute(true); return; } var flipX = true; if(stream.videoType == "screen") flipX = false; var localVideo = document.createElement('video'); localVideo.id = 'localVideo_' + APP.RTC.getStreamID(stream.getOriginalStream()); localVideo.autoplay = true; localVideo.volume = 0; // is it required if audio is separated ? localVideo.oncontextmenu = function () { return false; }; var localVideoContainer = document.getElementById('localVideoWrapper'); localVideoContainer.appendChild(localVideo); var localVideoSelector = $('#' + localVideo.id); // Add click handler to both video and video wrapper elements in case // there's no video. localVideoSelector.click(localVideoClick); // Flip video x axis if needed flipXLocalVideo = flipX; if (flipX) { localVideoSelector.addClass("flipVideoX"); } // Attach WebRTC stream APP.RTC.attachMediaStream(localVideoSelector, stream.getOriginalStream()); // Add stream ended handler stream.getOriginalStream().onended = function () { localVideoContainer.removeChild(localVideo); VideoLayout.updateRemovedVideo(APP.RTC.getVideoSrc(localVideo)); }; localVideoSrc = APP.RTC.getVideoSrc(localVideo); var myResourceJid = APP.xmpp.myResource(); VideoLayout.updateLargeVideo(localVideoSrc, 0, myResourceJid); }; my.mucJoined = function () { var myResourceJid = APP.xmpp.myResource(); if (!largeVideoState.userResourceJid) VideoLayout.updateLargeVideo(localVideoSrc, 0, myResourceJid, true); }; /** * Adds or removes icons for not available camera and microphone. * @param resourceJid the jid of user * @param devices available devices */ my.setDeviceAvailabilityIcons = function (resourceJid, devices) { if(!devices) return; var container = null if(!resourceJid) { container = $("#localVideoContainer")[0]; } else { container = $("#participant_" + resourceJid)[0]; } if(!container) return; $("#" + container.id + " > .noMic").remove(); $("#" + container.id + " > .noVideo").remove(); if(!devices.audio) { container.appendChild(document.createElement("div")).setAttribute("class","noMic"); } if(!devices.video) { container.appendChild(document.createElement("div")).setAttribute("class","noVideo"); } if(!devices.audio && !devices.video) { $("#" + container.id + " > .noMic").css("background-position", "75%"); $("#" + container.id + " > .noVideo").css("background-position", "25%"); $("#" + container.id + " > .noVideo").css("background-color", "transparent"); } } /** * Checks if removed video is currently displayed and tries to display * another one instead. * @param removedVideoSrc src stream identifier of the video. */ my.updateRemovedVideo = function(removedVideoSrc) { if (removedVideoSrc === APP.RTC.getVideoSrc($('#largeVideo')[0])) { // this is currently displayed as large // pick the last visible video in the row // if nobody else is left, this picks the local video var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video') .get(0); if (!pick) { console.info("Last visible video no longer exists"); pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0); if (!pick || !APP.RTC.getVideoSrc(pick)) { // Try local video console.info("Fallback to local video..."); pick = $('#remoteVideos>span>span>video').get(0); } } var src = null, volume = null; // mute if localvideo if (pick) { var container = pick.parentNode; src = APP.RTC.getVideoSrc(pick); volume = pick.volume; } else { console.warn("Failed to elect large video"); container = $('#remoteVideos>span[id!="mixedstream"]:visible:last').get(0); } var jid = null; if(container) { if(container.id == "localVideoWrapper") { jid = APP.xmpp.myResource(); } else { jid = VideoLayout.getPeerContainerResourceJid(container); } } else return; VideoLayout.updateLargeVideo(src, volume, jid); } }; my.onRemoteStreamAdded = function (stream) { var container; var remotes = document.getElementById('remoteVideos'); if (stream.peerjid) { VideoLayout.ensurePeerContainerExists(stream.peerjid); container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(stream.peerjid)); } else { var id = stream.getOriginalStream().id; if (id !== 'mixedmslabel' // FIXME: default stream is added always with new focus // (to be investigated) && id !== 'default') { console.error('can not associate stream', id, 'with a participant'); // We don't want to add it here since it will cause troubles return; } // FIXME: for the mixed ms we dont need a video -- currently container = document.createElement('span'); container.id = 'mixedstream'; container.className = 'videocontainer'; remotes.appendChild(container); } if (container) { VideoLayout.addRemoteStreamElement( container, stream.sid, stream.getOriginalStream(), stream.peerjid, stream.ssrc); } } my.getLargeVideoState = function () { return largeVideoState; }; /** * Updates the large video with the given new video source. */ my.updateLargeVideo = function(newSrc, vol, resourceJid, forceUpdate) { console.log('hover in', newSrc, resourceJid); if (APP.RTC.getVideoSrc($('#largeVideo')[0]) !== newSrc || forceUpdate) { $('#activeSpeaker').css('visibility', 'hidden'); // Due to the simulcast the localVideoSrc may have changed when the // fadeOut event triggers. In that case the getJidFromVideoSrc and // isVideoSrcDesktop methods will not function correctly. // // Also, again due to the simulcast, the updateLargeVideo method can // be called multiple times almost simultaneously. Therefore, we // store the state here and update only once. largeVideoState.newSrc = newSrc; largeVideoState.isVisible = $('#largeVideo').is(':visible'); largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop( APP.xmpp.findJidFromResource(resourceJid)); if(largeVideoState.userResourceJid) { largeVideoState.oldResourceJid = largeVideoState.userResourceJid; } else { largeVideoState.oldResourceJid = null; } largeVideoState.userResourceJid = resourceJid; // Screen stream is already rotated largeVideoState.flipX = (newSrc === localVideoSrc) && flipXLocalVideo; if (largeVideoState.oldResourceJid !== largeVideoState.userResourceJid) { // we want the notification to trigger even if userJid is undefined, // or null. eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, largeVideoState.userResourceJid); } $('#largeVideo').fadeOut(300, function () { Avatar.updateActiveSpeakerAvatarSrc( APP.xmpp.findJidFromResource( largeVideoState.userResourceJid)); APP.RTC.setVideoSrc($('#largeVideo')[0], largeVideoState.newSrc); var videoTransform = document.getElementById('largeVideo') .style.webkitTransform; if (largeVideoState.flipX && videoTransform !== 'scaleX(-1)') { document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)"; } else if (!largeVideoState.flipX && videoTransform === 'scaleX(-1)') { document.getElementById('largeVideo').style.webkitTransform = "none"; } // Change the way we'll be measuring and positioning large video VideoLayout.getVideoSize = largeVideoState.isDesktop ? getDesktopVideoSize : getCameraVideoSize; VideoLayout.getVideoPosition = largeVideoState.isDesktop ? getDesktopVideoPosition : getCameraVideoPosition; // Only if the large video is currently visible. // Disable previous dominant speaker video. if (largeVideoState.oldResourceJid) { VideoLayout.enableDominantSpeaker( largeVideoState.oldResourceJid, false); } // Enable new dominant speaker in the remote videos section. if (largeVideoState.userResourceJid) { VideoLayout.enableDominantSpeaker( largeVideoState.userResourceJid, true); } if (largeVideoState.isVisible) { // using "this" should be ok because we're called // from within the fadeOut event. $(this).fadeIn(300); } Avatar.showUserAvatar( APP.xmpp.findJidFromResource( largeVideoState.oldResourceJid)); }); } else { Avatar.showUserAvatar( APP.xmpp.findJidFromResource( largeVideoState.userResourceJid)); } }; my.handleVideoThumbClicked = function(videoSrc, noPinnedEndpointChangedEvent, resourceJid) { // Restore style for previously focused video var oldContainer = null; if(focusedVideoInfo) { var focusResourceJid = focusedVideoInfo.resourceJid; oldContainer = getParticipantContainer(focusResourceJid); } if (oldContainer) { oldContainer.removeClass("videoContainerFocused"); } // Unlock current focused. if (focusedVideoInfo && focusedVideoInfo.src === videoSrc) { focusedVideoInfo = null; var dominantSpeakerVideo = null; // Enable the currently set dominant speaker. if (currentDominantSpeaker) { dominantSpeakerVideo = $('#participant_' + currentDominantSpeaker + '>video') .get(0); if (dominantSpeakerVideo) { VideoLayout.updateLargeVideo( APP.RTC.getVideoSrc(dominantSpeakerVideo), 1, currentDominantSpeaker); } } if (!noPinnedEndpointChangedEvent) { eventEmitter.emit(UIEvents.PINNED_ENDPOINT); } return; } // Lock new video focusedVideoInfo = { src: videoSrc, resourceJid: resourceJid }; // Update focused/pinned interface. if (resourceJid) { var container = getParticipantContainer(resourceJid); container.addClass("videoContainerFocused"); if (!noPinnedEndpointChangedEvent) { eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid); } } if ($('#largeVideo').attr('src') === videoSrc && VideoLayout.isLargeVideoOnTop()) { return; } // Triggers a "video.selected" event. The "false" parameter indicates // this isn't a prezi. $(document).trigger("video.selected", [false]); VideoLayout.updateLargeVideo(videoSrc, 1, resourceJid); $('audio').each(function (idx, el) { if (el.id.indexOf('mixedmslabel') !== -1) { el.volume = 0; el.volume = 1; } }); }; /** * Positions the large video. * * @param videoWidth the stream video width * @param videoHeight the stream video height */ my.positionLarge = function (videoWidth, videoHeight) { var videoSpaceWidth = $('#videospace').width(); var videoSpaceHeight = window.innerHeight; var videoSize = VideoLayout.getVideoSize(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight); var largeVideoWidth = videoSize[0]; var largeVideoHeight = videoSize[1]; var videoPosition = VideoLayout.getVideoPosition(largeVideoWidth, largeVideoHeight, videoSpaceWidth, videoSpaceHeight); var horizontalIndent = videoPosition[0]; var verticalIndent = videoPosition[1]; positionVideo($('#largeVideo'), largeVideoWidth, largeVideoHeight, horizontalIndent, verticalIndent); }; /** * Shows/hides the large video. */ my.setLargeVideoVisible = function(isVisible) { var resourceJid = largeVideoState.userResourceJid; if (isVisible) { $('#largeVideo').css({visibility: 'visible'}); $('.watermark').css({visibility: 'visible'}); VideoLayout.enableDominantSpeaker(resourceJid, true); } else { $('#largeVideo').css({visibility: 'hidden'}); $('#activeSpeaker').css('visibility', 'hidden'); $('.watermark').css({visibility: 'hidden'}); VideoLayout.enableDominantSpeaker(resourceJid, false); if(focusedVideoInfo) { var focusResourceJid = focusedVideoInfo.resourceJid; var oldContainer = getParticipantContainer(focusResourceJid); if (oldContainer && oldContainer.length > 0) { oldContainer.removeClass("videoContainerFocused"); } focusedVideoInfo = null; if(focusResourceJid) { Avatar.showUserAvatar( APP.xmpp.findJidFromResource(focusResourceJid)); } } } }; /** * Indicates if the large video is currently visible. * * @return true if visible, false - otherwise */ my.isLargeVideoVisible = function() { return $('#largeVideo').is(':visible'); }; my.isLargeVideoOnTop = function () { var Etherpad = require("../etherpad/Etherpad"); var Prezi = require("../prezi/Prezi"); return !Prezi.isPresentationVisible() && !Etherpad.isVisible(); }; /** * Checks if container for participant identified by given peerJid exists * in the document and creates it eventually. * * @param peerJid peer Jid to check. * @param userId user email or id for setting the avatar * * @return Returns true if the peer container exists, * false - otherwise */ my.ensurePeerContainerExists = function(peerJid, userId) { ContactList.ensureAddContact(peerJid, userId); var resourceJid = Strophe.getResourceFromJid(peerJid); var videoSpanId = 'participant_' + resourceJid; if (!$('#' + videoSpanId).length) { var container = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId, userId); Avatar.setUserAvatar(peerJid, userId); // Set default display name. setDisplayName(videoSpanId); VideoLayout.connectionIndicators[videoSpanId] = new ConnectionIndicator(container, peerJid, VideoLayout); var nickfield = document.createElement('span'); nickfield.className = "nick"; nickfield.appendChild(document.createTextNode(resourceJid)); container.appendChild(nickfield); // In case this is not currently in the last n we don't show it. if (localLastNCount && localLastNCount > 0 && $('#remoteVideos>span').length >= localLastNCount + 2) { showPeerContainer(resourceJid, 'hide'); } else VideoLayout.resizeThumbnails(); } }; my.addRemoteVideoContainer = function(peerJid, spanId) { var container = document.createElement('span'); container.id = spanId; container.className = 'videocontainer'; var remotes = document.getElementById('remoteVideos'); remotes.appendChild(container); // If the peerJid is null then this video span couldn't be directly // associated with a participant (this could happen in the case of prezi). if (APP.xmpp.isModerator() && peerJid !== null) addRemoteVideoMenu(peerJid, container); AudioLevels.updateAudioLevelCanvas(peerJid, VideoLayout); return container; }; /** * Creates an audio or video stream element. */ my.createStreamElement = function (sid, stream) { var isVideo = stream.getVideoTracks().length > 0; var element = isVideo ? document.createElement('video') : document.createElement('audio'); var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + APP.RTC.getStreamID(stream); element.id = id; element.autoplay = true; element.oncontextmenu = function () { return false; }; return element; }; my.addRemoteStreamElement = function (container, sid, stream, peerJid, thessrc) { var newElementId = null; var isVideo = stream.getVideoTracks().length > 0; if (container) { var streamElement = VideoLayout.createStreamElement(sid, stream); newElementId = streamElement.id; container.appendChild(streamElement); var sel = $('#' + newElementId); sel.hide(); // If the container is currently visible we attach the stream. if (!isVideo || (container.offsetParent !== null && isVideo)) { APP.RTC.attachMediaStream(sel, stream); if (isVideo) waitForRemoteVideo(sel, thessrc, stream, peerJid); } stream.onended = function () { console.log('stream ended', this); VideoLayout.removeRemoteStreamElement( stream, isVideo, container, newElementId); // NOTE(gp) it seems that under certain circumstances, the // onended event is not fired and thus the contact list is not // updated. // // The onended event of a stream should be fired when the SSRCs // corresponding to that stream are removed from the SDP; but // this doesn't seem to always be the case, resulting in ghost // contacts. // // In an attempt to fix the ghost contacts problem, I'm moving // the removeContact() method call in app.js, inside the // 'muc.left' event handler. //if (peerJid) // ContactList.removeContact(peerJid); }; // Add click handler. container.onclick = function (event) { /* * FIXME It turns out that videoThumb may not exist (if there is * no actual video). */ var videoThumb = $('#' + container.id + '>video').get(0); if (videoThumb) { VideoLayout.handleVideoThumbClicked( APP.RTC.getVideoSrc(videoThumb), false, Strophe.getResourceFromJid(peerJid)); } event.stopPropagation(); event.preventDefault(); return false; }; // Add hover handler $(container).hover( function() { VideoLayout.showDisplayName(container.id, true); }, function() { var videoSrc = null; if ($('#' + container.id + '>video') && $('#' + container.id + '>video').length > 0) { videoSrc = APP.RTC.getVideoSrc($('#' + container.id + '>video').get(0)); } // If the video has been "pinned" by the user we want to // keep the display name on place. if (!VideoLayout.isLargeVideoVisible() || videoSrc !== APP.RTC.getVideoSrc($('#largeVideo')[0])) VideoLayout.showDisplayName(container.id, false); } ); } return newElementId; }; /** * Removes the remote stream element corresponding to the given stream and * parent container. * * @param stream the stream * @param isVideo true if given stream is a video one. * @param container */ my.removeRemoteStreamElement = function (stream, isVideo, container, id) { if (!container) return; var select = null; var removedVideoSrc = null; if (isVideo) { select = $('#' + id); removedVideoSrc = APP.RTC.getVideoSrc(select.get(0)); } else select = $('#' + container.id + '>audio'); // Mark video as removed to cancel waiting loop(if video is removed // before has started) select.removed = true; select.remove(); var audioCount = $('#' + container.id + '>audio').length; var videoCount = $('#' + container.id + '>video').length; if (!audioCount && !videoCount) { console.log("Remove whole user", container.id); if(VideoLayout.connectionIndicators[container.id]) VideoLayout.connectionIndicators[container.id].remove(); // Remove whole container container.remove(); VideoLayout.resizeThumbnails(); } if (removedVideoSrc) VideoLayout.updateRemovedVideo(removedVideoSrc); }; /** * Show/hide peer container for the given resourceJid. */ function showPeerContainer(resourceJid, state) { var peerContainer = $('#participant_' + resourceJid); if (!peerContainer) return; var isHide = state === 'hide'; var resizeThumbnails = false; if (!isHide) { if (!peerContainer.is(':visible')) { resizeThumbnails = true; peerContainer.show(); } var jid = APP.xmpp.findJidFromResource(resourceJid); if (state == 'show') { // peerContainer.css('-webkit-filter', ''); Avatar.showUserAvatar(jid, false); } else // if (state == 'avatar') { // peerContainer.css('-webkit-filter', 'grayscale(100%)'); Avatar.showUserAvatar(jid, true); } } else if (peerContainer.is(':visible') && isHide) { resizeThumbnails = true; peerContainer.hide(); if(VideoLayout.connectionIndicators['participant_' + resourceJid]) VideoLayout.connectionIndicators['participant_' + resourceJid].hide(); } if (resizeThumbnails) { VideoLayout.resizeThumbnails(); } // 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); }; my.inputDisplayNameHandler = function (name) { NicknameHandler.setNickname(name); if (!$('#localDisplayName').is(":visible")) { if (NicknameHandler.getNickname()) { var meHTML = APP.translation.generateTranslatonHTML("me"); $('#localDisplayName').html(NicknameHandler.getNickname() + " (" + meHTML + ")"); } else { var defaultHTML = APP.translation.generateTranslatonHTML( interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME); $('#localDisplayName') .html(defaultHTML); } $('#localDisplayName').show(); } $('#editDisplayName').hide(); }; /** * Shows/hides the display name on the remote video. * @param videoSpanId the identifier of the video span element * @param isShow indicates if the display name should be shown or hidden */ my.showDisplayName = function(videoSpanId, isShow) { var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0); if (isShow) { if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) nameSpan.setAttribute("style", "display:inline-block;"); } else { if (nameSpan) nameSpan.setAttribute("style", "display:none;"); } }; /** * Shows the presence status message for the given video. */ my.setPresenceStatus = function (videoSpanId, statusMsg) { if (!$('#' + videoSpanId).length) { // No container return; } var statusSpan = $('#' + videoSpanId + '>span.status'); if (!statusSpan.length) { //Add status span statusSpan = document.createElement('span'); statusSpan.className = 'status'; statusSpan.id = videoSpanId + '_status'; $('#' + videoSpanId)[0].appendChild(statusSpan); statusSpan = $('#' + videoSpanId + '>span.status'); } // Display status if (statusMsg && statusMsg.length) { $('#' + videoSpanId + '_status').text(statusMsg); statusSpan.get(0).setAttribute("style", "display:inline-block;"); } else { // Hide statusSpan.get(0).setAttribute("style", "display:none;"); } }; /** * Shows a visual indicator for the moderator of the conference. */ my.showModeratorIndicator = function () { var isModerator = APP.xmpp.isModerator(); if (isModerator) { var indicatorSpan = $('#localVideoContainer .focusindicator'); if (indicatorSpan.children().length === 0) { createModeratorIndicatorElement(indicatorSpan[0]); //translates text in focus indicator APP.translation.translateElement($('#localVideoContainer .focusindicator')); } } var members = APP.xmpp.getMembers(); Object.keys(members).forEach(function (jid) { if (Strophe.getResourceFromJid(jid) === 'focus') { // Skip server side focus return; } var resourceJid = Strophe.getResourceFromJid(jid); var videoSpanId = 'participant_' + resourceJid; var videoContainer = document.getElementById(videoSpanId); if (!videoContainer) { console.error("No video container for " + jid); return; } var member = members[jid]; if (member.role === 'moderator') { // Remove menu if peer is moderator var menuSpan = $('#' + videoSpanId + '>span.remotevideomenu'); if (menuSpan.length) { removeRemoteVideoMenu(videoSpanId); } // Show moderator indicator var indicatorSpan = $('#' + videoSpanId + ' .focusindicator'); if (!indicatorSpan || indicatorSpan.length === 0) { indicatorSpan = document.createElement('span'); indicatorSpan.className = 'focusindicator'; videoContainer.appendChild(indicatorSpan); createModeratorIndicatorElement(indicatorSpan); //translates text in focus indicators APP.translation.translateElement($('#' + videoSpanId + ' .focusindicator')); } } else if (isModerator) { // We are moderator, but user is not - add menu if ($('#remote_popupmenu_' + resourceJid).length <= 0) { addRemoteVideoMenu( jid, document.getElementById('participant_' + resourceJid)); } } }); }; /** * Shows video muted indicator over small videos. */ my.showVideoIndicator = function(videoSpanId, isMuted) { var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted'); if (isMuted === 'false') { if (videoMutedSpan.length > 0) { videoMutedSpan.remove(); } } else { if(videoMutedSpan.length == 0) { videoMutedSpan = document.createElement('span'); videoMutedSpan.className = 'videoMuted'; $('#' + videoSpanId)[0].appendChild(videoMutedSpan); var mutedIndicator = document.createElement('i'); mutedIndicator.className = 'icon-camera-disabled'; UIUtil.setTooltip(mutedIndicator, "videothumbnail.videomute", "top"); videoMutedSpan.appendChild(mutedIndicator); //translate texts for muted indicator APP.translation.translateElement($('#' + videoSpanId + " > span > i")); } VideoLayout.updateMutePosition(videoSpanId); } }; my.updateMutePosition = function (videoSpanId) { var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted'); var connectionIndicator = $('#' + videoSpanId + '>div.connectionindicator'); var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted'); if(connectionIndicator.length > 0 && connectionIndicator[0].style.display != "none") { audioMutedSpan.css({right: "23px"}); videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"}); } else { audioMutedSpan.css({right: "0px"}); videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"}); } } /** * Shows audio muted indicator over small videos. * @param {string} isMuted */ my.showAudioIndicator = function(videoSpanId, isMuted) { var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted'); if (isMuted === 'false') { if (audioMutedSpan.length > 0) { audioMutedSpan.popover('hide'); audioMutedSpan.remove(); } } else { if(audioMutedSpan.length == 0 ) { audioMutedSpan = document.createElement('span'); audioMutedSpan.className = 'audioMuted'; UIUtil.setTooltip(audioMutedSpan, "videothumbnail.mute", "top"); $('#' + videoSpanId)[0].appendChild(audioMutedSpan); APP.translation.translateElement($('#' + videoSpanId + " > span")); var mutedIndicator = document.createElement('i'); mutedIndicator.className = 'icon-mic-disabled'; audioMutedSpan.appendChild(mutedIndicator); } VideoLayout.updateMutePosition(videoSpanId); } }; /* * Shows or hides the audio muted indicator over the local thumbnail video. * @param {boolean} isMuted */ my.showLocalAudioIndicator = function(isMuted) { VideoLayout.showAudioIndicator('localVideoContainer', isMuted.toString()); }; /** * Resizes the large video container. */ my.resizeLargeVideoContainer = function () { Chat.resizeChat(); var availableHeight = window.innerHeight; var availableWidth = UIUtil.getAvailableVideoWidth(); if (availableWidth < 0 || availableHeight < 0) return; $('#videospace').width(availableWidth); $('#videospace').height(availableHeight); $('#largeVideoContainer').width(availableWidth); $('#largeVideoContainer').height(availableHeight); var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE; var top = availableHeight / 2 - avatarSize / 4 * 3; $('#activeSpeaker').css('top', top); VideoLayout.resizeThumbnails(); }; /** * Resizes thumbnails. */ my.resizeThumbnails = function() { var videoSpaceWidth = $('#remoteVideos').width(); var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth); var width = thumbnailSize[0]; var height = thumbnailSize[1]; // size videos so that while keeping AR and max height, we have a // nice fit $('#remoteVideos').height(height); $('#remoteVideos>span').width(width); $('#remoteVideos>span').height(height); $('.userAvatar').css('left', (width - height) / 2); $(document).trigger("remotevideo.resized", [width, height]); }; /** * Enables the dominant speaker UI. * * @param resourceJid the jid indicating the video element to * activate/deactivate * @param isEnable indicates if the dominant speaker should be enabled or * disabled */ my.enableDominantSpeaker = function(resourceJid, isEnable) { var videoSpanId = null; var videoContainerId = null; if (resourceJid === APP.xmpp.myResource()) { videoSpanId = 'localVideoWrapper'; videoContainerId = 'localVideoContainer'; } else { videoSpanId = 'participant_' + resourceJid; videoContainerId = videoSpanId; } var displayName = resourceJid; var nameSpan = $('#' + videoContainerId + '>span.displayname'); if (nameSpan.length > 0) displayName = nameSpan.html(); console.log("UI enable dominant speaker", displayName, resourceJid, isEnable); videoSpan = document.getElementById(videoContainerId); if (!videoSpan) { return; } var video = $('#' + videoSpanId + '>video'); if (video && video.length > 0) { if (isEnable) { var isLargeVideoVisible = VideoLayout.isLargeVideoOnTop(); VideoLayout.showDisplayName(videoContainerId, isLargeVideoVisible); if (!videoSpan.classList.contains("dominantspeaker")) videoSpan.classList.add("dominantspeaker"); } else { VideoLayout.showDisplayName(videoContainerId, false); if (videoSpan.classList.contains("dominantspeaker")) videoSpan.classList.remove("dominantspeaker"); } } Avatar.showUserAvatar( APP.xmpp.findJidFromResource(resourceJid)); }; /** * Calculates the thumbnail size. * * @param videoSpaceWidth the width of the video space */ my.calculateThumbnailSize = function (videoSpaceWidth) { // Calculate the available height, which is the inner window height minus // 39px for the header minus 2px for the delimiter lines on the top and // bottom of the large video, minus the 36px space inside the remoteVideos // container used for highlighting shadow. var availableHeight = 100; var numvids = $('#remoteVideos>span:visible').length; if (localLastNCount && localLastNCount > 0) { numvids = Math.min(localLastNCount + 1, numvids); } // Remove the 3px borders arround videos and border around the remote // videos area and the 4 pixels between the local video and the others //TODO: Find out where the 4 pixels come from and remove them var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4; var availableWidth = availableWinWidth / numvids; var aspectRatio = 16.0 / 9.0; var maxHeight = Math.min(160, availableHeight); availableHeight = Math.min(maxHeight, availableWidth / aspectRatio); if (availableHeight < availableWidth / aspectRatio) { availableWidth = Math.floor(availableHeight * aspectRatio); } return [availableWidth, availableHeight]; }; /** * Updates the remote video menu. * * @param jid the jid indicating the video for which we're adding a menu. * @param isMuted indicates the current mute state */ my.updateRemoteVideoMenu = function(jid, isMuted) { var muteMenuItem = $('#remote_popupmenu_' + Strophe.getResourceFromJid(jid) + '>li>a.mutelink'); var mutedIndicator = ""; if (muteMenuItem.length) { var muteLink = muteMenuItem.get(0); if (isMuted === 'true') { muteLink.innerHTML = mutedIndicator + ' Muted'; muteLink.className = 'mutelink disabled'; } else { muteLink.innerHTML = mutedIndicator + ' Mute'; muteLink.className = 'mutelink'; } } }; /** * Returns the current dominant speaker resource jid. */ my.getDominantSpeakerResourceJid = function () { return currentDominantSpeaker; }; /** * Returns the corresponding resource jid to the given peer container * DOM element. * * @return the corresponding resource jid to the given peer container * DOM element */ my.getPeerContainerResourceJid = function (containerElement) { var i = containerElement.id.indexOf('participant_'); if (i >= 0) return containerElement.id.substring(i + 12); }; /** * On contact list item clicked. */ $(ContactList).bind('contactclicked', function(event, jid) { if (!jid) { return; } var resource = Strophe.getResourceFromJid(jid); var videoContainer = $("#participant_" + resource); if (videoContainer.length > 0) { var videoThumb = $('video', videoContainer).get(0); // It is not always the case that a videoThumb exists (if there is // no actual video). if (videoThumb) { if (videoThumb.src && videoThumb.src != '') { // We have a video src, great! Let's update the large video // now. VideoLayout.handleVideoThumbClicked( videoThumb.src, false, Strophe.getResourceFromJid(jid)); } else { // If we don't have a video src for jid, there's absolutely // no point in calling handleVideoThumbClicked; Quite // simply, it won't work because it needs an src to attach // to the large video. // // Instead, we trigger the pinned endpoint changed event to // let the bridge adjust its lastN set for myjid and store // the pinned user in the lastNPickupJid variable to be // picked up later by the lastN changed event handler. lastNPickupJid = jid; eventEmitter.emit(UIEvents.PINNED_ENDPOINT, Strophe.getResourceFromJid(jid)); } } else if (jid == APP.xmpp.myJid()) { $("#localVideoContainer").click(); } } }); /** * On audio muted event. */ $(document).bind('audiomuted.muc', function (event, jid, isMuted) { /* // FIXME: but focus can not mute in this case ? - check if (jid === xmpp.myJid()) { // The local mute indicator is controlled locally return; }*/ var videoSpanId = null; if (jid === APP.xmpp.myJid()) { videoSpanId = 'localVideoContainer'; } else { VideoLayout.ensurePeerContainerExists(jid); videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); } mutedAudios[jid] = isMuted; if (APP.xmpp.isModerator()) { VideoLayout.updateRemoteVideoMenu(jid, isMuted); } if (videoSpanId) VideoLayout.showAudioIndicator(videoSpanId, isMuted); }); /** * On video muted event. */ $(document).bind('videomuted.muc', function (event, jid, value) { var isMuted = (value === "true"); if(jid !== APP.xmpp.myJid() && !APP.RTC.muteRemoteVideoStream(jid, isMuted)) return; Avatar.showUserAvatar(jid, isMuted); var videoSpanId = null; if (jid === APP.xmpp.myJid()) { videoSpanId = 'localVideoContainer'; } else { VideoLayout.ensurePeerContainerExists(jid); videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); } if (videoSpanId) VideoLayout.showVideoIndicator(videoSpanId, value); }); /** * Display name changed. */ my.onDisplayNameChanged = function (jid, displayName, status) { if (jid === 'localVideoContainer' || jid === APP.xmpp.myJid()) { setDisplayName('localVideoContainer', displayName); } else { VideoLayout.ensurePeerContainerExists(jid); setDisplayName( 'participant_' + Strophe.getResourceFromJid(jid), displayName, status); } }; /** * On dominant speaker changed event. */ my.onDominantSpeakerChanged = function (resourceJid) { // We ignore local user events. if (resourceJid === APP.xmpp.myResource()) return; var members = APP.xmpp.getMembers(); // Update the current dominant speaker. if (resourceJid !== currentDominantSpeaker) { var oldSpeakerVideoSpanId = "participant_" + currentDominantSpeaker, newSpeakerVideoSpanId = "participant_" + resourceJid; var currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker); var newJID = APP.xmpp.findJidFromResource(resourceJid); if(currentDominantSpeaker && (!members || !members[currentJID] || !members[currentJID].displayName)) { setDisplayName(oldSpeakerVideoSpanId, null); } if(resourceJid && (!members || !members[newJID] || !members[newJID].displayName)) { setDisplayName(newSpeakerVideoSpanId, null, interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME); } currentDominantSpeaker = resourceJid; } else { return; } // Obtain container for new dominant speaker. var container = document.getElementById( 'participant_' + resourceJid); // Local video will not have container found, but that's ok // since we don't want to switch to local video. if (container && !focusedVideoInfo) { var video = container.getElementsByTagName("video"); // Update the large video if the video source is already available, // otherwise wait for the "videoactive.jingle" event. if (video.length && video[0].currentTime > 0) { VideoLayout.updateLargeVideo( APP.RTC.getVideoSrc(video[0]), 1, resourceJid); } } }; /** * On last N change event. * * @param lastNEndpoints the list of last N endpoints * @param endpointsEnteringLastN the list currently entering last N * endpoints */ my.onLastNEndpointsChanged = function ( lastNEndpoints, endpointsEnteringLastN, stream) { if (lastNCount !== lastNEndpoints.length) lastNCount = lastNEndpoints.length; lastNEndpointsCache = lastNEndpoints; // Say A, B, C, D, E, and F are in a conference and LastN = 3. // // If LastN drops to, say, 2, because of adaptivity, then E should see // thumbnails for A, B and C. A and B are in E's server side LastN set, // so E sees them. C is only in E's local LastN set. // // If F starts talking and LastN = 3, then E should see thumbnails for // F, A, B. B gets "ejected" from E's server side LastN set, but it // enters E's local LastN ejecting C. // Increase the local LastN set size, if necessary. if (lastNCount > localLastNCount) { localLastNCount = lastNCount; } // Update the local LastN set preserving the order in which the // endpoints appeared in the LastN/local LastN set. var nextLocalLastNSet = lastNEndpoints.slice(0); for (var i = 0; i < localLastNSet.length; i++) { if (nextLocalLastNSet.length >= localLastNCount) { break; } var resourceJid = localLastNSet[i]; if (nextLocalLastNSet.indexOf(resourceJid) === -1) { nextLocalLastNSet.push(resourceJid); } } localLastNSet = nextLocalLastNSet; var updateLargeVideo = false; // Handle LastN/local LastN changes. $('#remoteVideos>span').each(function( index, element ) { var resourceJid = VideoLayout.getPeerContainerResourceJid(element); var isReceived = true; if (resourceJid && lastNEndpoints.indexOf(resourceJid) < 0 && localLastNSet.indexOf(resourceJid) < 0) { console.log("Remove from last N", resourceJid); showPeerContainer(resourceJid, 'hide'); isReceived = false; } else if (resourceJid && $('#participant_' + resourceJid).is(':visible') && lastNEndpoints.indexOf(resourceJid) < 0 && localLastNSet.indexOf(resourceJid) >= 0) { showPeerContainer(resourceJid, 'avatar'); isReceived = false; } if (!isReceived) { // resourceJid has dropped out of the server side lastN set, so // it is no longer being received. If resourceJid was being // displayed in the large video we have to switch to another // user. var largeVideoResource = largeVideoState.userResourceJid; if (!updateLargeVideo && resourceJid === largeVideoResource) { updateLargeVideo = true; } } }); if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0) endpointsEnteringLastN = lastNEndpoints; if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) { endpointsEnteringLastN.forEach(function (resourceJid) { var isVisible = $('#participant_' + resourceJid).is(':visible'); showPeerContainer(resourceJid, 'show'); if (!isVisible) { console.log("Add to last N", resourceJid); var jid = APP.xmpp.findJidFromResource(resourceJid); var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]; var sel = $('#participant_' + resourceJid + '>video'); APP.RTC.attachMediaStream(sel, mediaStream.stream); if (lastNPickupJid == mediaStream.peerjid) { // Clean up the lastN pickup jid. lastNPickupJid = null; // Don't fire the events again, they've already // been fired in the contact list click handler. VideoLayout.handleVideoThumbClicked( $(sel).attr('src'), false, Strophe.getResourceFromJid(mediaStream.peerjid)); updateLargeVideo = false; } waitForRemoteVideo(sel, mediaStream.ssrc, mediaStream.stream, resourceJid); } }) } // The endpoint that was being shown in the large video has dropped out // of the lastN set and there was no lastN pickup jid. We need to update // the large video now. if (updateLargeVideo) { var resource, container, src; var myResource = APP.xmpp.myResource(); // Find out which endpoint to show in the large video. for (var i = 0; i < lastNEndpoints.length; i++) { resource = lastNEndpoints[i]; if (!resource || resource === myResource) continue; container = $("#participant_" + resource); if (container.length == 0) continue; src = $('video', container).attr('src'); if (!src) continue; // videoSrcToSsrc needs to be update for this call to succeed. VideoLayout.updateLargeVideo(src, 1, resource); break; } } }; /** * Updates local stats * @param percent * @param object */ my.updateLocalConnectionStats = function (percent, object) { var resolution = null; if(object.resolution !== null) { resolution = object.resolution; object.resolution = resolution[APP.xmpp.myJid()]; delete resolution[APP.xmpp.myJid()]; } updateStatsIndicator("localVideoContainer", percent, object); for(var jid in resolution) { if(resolution[jid] === null) continue; var id = 'participant_' + Strophe.getResourceFromJid(jid); if(VideoLayout.connectionIndicators[id]) { VideoLayout.connectionIndicators[id].updateResolution(resolution[jid]); } } }; /** * Updates remote stats. * @param jid the jid associated with the stats * @param percent the connection quality percent * @param object the stats data */ my.updateConnectionStats = function (jid, percent, object) { var resourceJid = Strophe.getResourceFromJid(jid); var videoSpanId = 'participant_' + resourceJid; updateStatsIndicator(videoSpanId, percent, object); }; /** * Removes the connection * @param jid */ my.removeConnectionIndicator = function (jid) { if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)]) VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].remove(); }; /** * Hides the connection indicator * @param jid */ my.hideConnectionIndicator = function (jid) { if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)]) VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].hide(); }; /** * Hides all the indicators */ my.onStatsStop = function () { for(var indicator in VideoLayout.connectionIndicators) { VideoLayout.connectionIndicators[indicator].hideIndicator(); } }; my.participantLeft = function (jid) { // Unlock large video var resourceJid = Strophe.getResourceFromJid(jid); if (focusedVideoInfo && focusedVideoInfo.resourceJid === resourceJid) { console.info("Focused video owner has left the conference"); focusedVideoInfo = null; } } my.onVideoTypeChanged = function (jid) { if(jid && Strophe.getResourceFromJid(jid) === largeVideoState.userResourceJid) { largeVideoState.isDesktop = APP.RTC.isVideoSrcDesktop(jid); VideoLayout.getVideoSize = largeVideoState.isDesktop ? getDesktopVideoSize : getCameraVideoSize; VideoLayout.getVideoPosition = largeVideoState.isDesktop ? getDesktopVideoPosition : getCameraVideoPosition; VideoLayout.positionLarge(null, null); } } return my; }(VideoLayout || {})); module.exports = VideoLayout; },{"../../../service/RTC/MediaStreamTypes":97,"../../../service/UI/UIEvents":102,"../audio_levels/AudioLevels":12,"../avatar/Avatar":16,"../etherpad/Etherpad":17,"../prezi/Prezi":18,"../side_pannels/chat/Chat":21,"../side_pannels/contactlist/ContactList":25,"../util/NicknameHandler":32,"../util/UIUtil":33,"./ConnectionIndicator":34}],36:[function(require,module,exports){ //var nouns = [ //]; var pluralNouns = [ "Aliens", "Animals", "Antelopes", "Ants", "Apes", "Apples", "Baboons", "Bacteria", "Badgers", "Bananas", "Bats", "Bears", "Birds", "Bonobos", "Brides", "Bugs", "Bulls", "Butterflies", "Cheetahs", "Cherries", "Chicken", "Children", "Chimps", "Clowns", "Cows", "Creatures", "Dinosaurs", "Dogs", "Dolphins", "Donkeys", "Dragons", "Ducks", "Dwarfs", "Eagles", "Elephants", "Elves", "FAIL", "Fathers", "Fish", "Flowers", "Frogs", "Fruit", "Fungi", "Galaxies", "Geese", "Goats", "Gorillas", "Hedgehogs", "Hippos", "Horses", "Hunters", "Insects", "Kids", "Knights", "Lemons", "Lemurs", "Leopards", "LifeForms", "Lions", "Lizards", "Mice", "Monkeys", "Monsters", "Mushrooms", "Octopodes", "Oranges", "Orangutans", "Organisms", "Pants", "Parrots", "Penguins", "People", "Pigeons", "Pigs", "Pineapples", "Plants", "Potatoes", "Priests", "Rats", "Reptiles", "Reptilians", "Rhinos", "Seagulls", "Sheep", "Siblings", "Snakes", "Spaghetti", "Spiders", "Squid", "Squirrels", "Stars", "Students", "Teachers", "Tigers", "Tomatoes", "Trees", "Vampires", "Vegetables", "Viruses", "Vulcans", "Warewolves", "Weasels", "Whales", "Witches", "Wizards", "Wolves", "Workers", "Worms", "Zebras" ]; //var places = [ //"Pub", "University", "Airport", "Library", "Mall", "Theater", "Stadium", "Office", "Show", "Gallows", "Beach", // "Cemetery", "Hospital", "Reception", "Restaurant", "Bar", "Church", "House", "School", "Square", "Village", // "Cinema", "Movies", "Party", "Restroom", "End", "Jail", "PostOffice", "Station", "Circus", "Gates", "Entrance", // "Bridge" //]; var verbs = [ "Abandon", "Adapt", "Advertise", "Answer", "Anticipate", "Appreciate", "Approach", "Argue", "Ask", "Bite", "Blossom", "Blush", "Breathe", "Breed", "Bribe", "Burn", "Calculate", "Clean", "Code", "Communicate", "Compute", "Confess", "Confiscate", "Conjugate", "Conjure", "Consume", "Contemplate", "Crawl", "Dance", "Delegate", "Devour", "Develop", "Differ", "Discuss", "Dissolve", "Drink", "Eat", "Elaborate", "Emancipate", "Estimate", "Expire", "Extinguish", "Extract", "FAIL", "Facilitate", "Fall", "Feed", "Finish", "Floss", "Fly", "Follow", "Fragment", "Freeze", "Gather", "Glow", "Grow", "Hex", "Hide", "Hug", "Hurry", "Improve", "Intersect", "Investigate", "Jinx", "Joke", "Jubilate", "Kiss", "Laugh", "Manage", "Meet", "Merge", "Move", "Object", "Observe", "Offer", "Paint", "Participate", "Party", "Perform", "Plan", "Pursue", "Pierce", "Play", "Postpone", "Pray", "Proclaim", "Question", "Read", "Reckon", "Rejoice", "Represent", "Resize", "Rhyme", "Scream", "Search", "Select", "Share", "Shoot", "Shout", "Signal", "Sing", "Skate", "Sleep", "Smile", "Smoke", "Solve", "Spell", "Steer", "Stink", "Substitute", "Swim", "Taste", "Teach", "Terminate", "Think", "Type", "Unite", "Vanish", "Worship" ]; var adverbs = [ "Absently", "Accurately", "Accusingly", "Adorably", "AllTheTime", "Alone", "Always", "Amazingly", "Angrily", "Anxiously", "Anywhere", "Appallingly", "Apparently", "Articulately", "Astonishingly", "Badly", "Barely", "Beautifully", "Blindly", "Bravely", "Brightly", "Briskly", "Brutally", "Calmly", "Carefully", "Casually", "Cautiously", "Cleverly", "Constantly", "Correctly", "Crazily", "Curiously", "Cynically", "Daily", "Dangerously", "Deliberately", "Delicately", "Desperately", "Discreetly", "Eagerly", "Easily", "Euphoricly", "Evenly", "Everywhere", "Exactly", "Expectantly", "Extensively", "FAIL", "Ferociously", "Fiercely", "Finely", "Flatly", "Frequently", "Frighteningly", "Gently", "Gloriously", "Grimly", "Guiltily", "Happily", "Hard", "Hastily", "Heroically", "High", "Highly", "Hourly", "Humbly", "Hysterically", "Immensely", "Impartially", "Impolitely", "Indifferently", "Intensely", "Jealously", "Jovially", "Kindly", "Lazily", "Lightly", "Loudly", "Lovingly", "Loyally", "Magnificently", "Malevolently", "Merrily", "Mightily", "Miserably", "Mysteriously", "NOT", "Nervously", "Nicely", "Nowhere", "Objectively", "Obnoxiously", "Obsessively", "Obviously", "Often", "Painfully", "Patiently", "Playfully", "Politely", "Poorly", "Precisely", "Promptly", "Quickly", "Quietly", "Randomly", "Rapidly", "Rarely", "Recklessly", "Regularly", "Remorsefully", "Responsibly", "Rudely", "Ruthlessly", "Sadly", "Scornfully", "Seamlessly", "Seldom", "Selfishly", "Seriously", "Shakily", "Sharply", "Sideways", "Silently", "Sleepily", "Slightly", "Slowly", "Slyly", "Smoothly", "Softly", "Solemnly", "Steadily", "Sternly", "Strangely", "Strongly", "Stunningly", "Surely", "Tenderly", "Thoughtfully", "Tightly", "Uneasily", "Vanishingly", "Violently", "Warmly", "Weakly", "Wearily", "Weekly", "Weirdly", "Well", "Well", "Wickedly", "Wildly", "Wisely", "Wonderfully", "Yearly" ]; var adjectives = [ "Abominable", "Accurate", "Adorable", "All", "Alleged", "Ancient", "Angry", "Angry", "Anxious", "Appalling", "Apparent", "Astonishing", "Attractive", "Awesome", "Baby", "Bad", "Beautiful", "Benign", "Big", "Bitter", "Blind", "Blue", "Bold", "Brave", "Bright", "Brisk", "Calm", "Camouflaged", "Casual", "Cautious", "Choppy", "Chosen", "Clever", "Cold", "Cool", "Crawly", "Crazy", "Creepy", "Cruel", "Curious", "Cynical", "Dangerous", "Dark", "Delicate", "Desperate", "Difficult", "Discreet", "Disguised", "Dizzy", "Dumb", "Eager", "Easy", "Edgy", "Electric", "Elegant", "Emancipated", "Enormous", "Euphoric", "Evil", "FAIL", "Fast", "Ferocious", "Fierce", "Fine", "Flawed", "Flying", "Foolish", "Foxy", "Freezing", "Funny", "Furious", "Gentle", "Glorious", "Golden", "Good", "Green", "Green", "Guilty", "Hairy", "Happy", "Hard", "Hasty", "Hazy", "Heroic", "Hostile", "Hot", "Humble", "Humongous", "Humorous", "Hysterical", "Idealistic", "Ignorant", "Immense", "Impartial", "Impolite", "Indifferent", "Infuriated", "Insightful", "Intense", "Interesting", "Intimidated", "Intriguing", "Jealous", "Jolly", "Jovial", "Jumpy", "Kind", "Laughing", "Lazy", "Liquid", "Lonely", "Longing", "Loud", "Loving", "Loyal", "Macabre", "Mad", "Magical", "Magnificent", "Malevolent", "Medieval", "Memorable", "Mere", "Merry", "Mighty", "Mischievous", "Miserable", "Modified", "Moody", "Most", "Mysterious", "Mystical", "Needy", "Nervous", "Nice", "Objective", "Obnoxious", "Obsessive", "Obvious", "Opinionated", "Orange", "Painful", "Passionate", "Perfect", "Pink", "Playful", "Poisonous", "Polite", "Poor", "Popular", "Powerful", "Precise", "Preserved", "Pretty", "Purple", "Quick", "Quiet", "Random", "Rapid", "Rare", "Real", "Reassuring", "Reckless", "Red", "Regular", "Remorseful", "Responsible", "Rich", "Rude", "Ruthless", "Sad", "Scared", "Scary", "Scornful", "Screaming", "Selfish", "Serious", "Shady", "Shaky", "Sharp", "Shiny", "Shy", "Simple", "Sleepy", "Slow", "Sly", "Small", "Smart", "Smelly", "Smiling", "Smooth", "Smug", "Sober", "Soft", "Solemn", "Square", "Square", "Steady", "Strange", "Strong", "Stunning", "Subjective", "Successful", "Surly", "Sweet", "Tactful", "Tense", "Thoughtful", "Tight", "Tiny", "Tolerant", "Uneasy", "Unique", "Unseen", "Warm", "Weak", "Weird", "WellCooked", "Wild", "Wise", "Witty", "Wonderful", "Worried", "Yellow", "Young", "Zealous" ]; //var pronouns = [ //]; //var conjunctions = [ //"And", "Or", "For", "Above", "Before", "Against", "Between" //]; /* * Maps a string (category name) to the array of words from that category. */ var CATEGORIES = { //"_NOUN_": nouns, "_PLURALNOUN_": pluralNouns, //"_PLACE_": places, "_VERB_": verbs, "_ADVERB_": adverbs, "_ADJECTIVE_": adjectives //"_PRONOUN_": pronouns, //"_CONJUNCTION_": conjunctions, }; var PATTERNS = [ "_ADJECTIVE__PLURALNOUN__VERB__ADVERB_" // BeautifulFungiOrSpaghetti //"_ADJECTIVE__PLURALNOUN__CONJUNCTION__PLURALNOUN_", // AmazinglyScaryToy //"_ADVERB__ADJECTIVE__NOUN_", // NeitherTrashNorRifle //"Neither_NOUN_Nor_NOUN_", //"Either_NOUN_Or_NOUN_", // EitherCopulateOrInvestigate //"Either_VERB_Or_VERB_", //"Neither_VERB_Nor_VERB_", //"The_ADJECTIVE__ADJECTIVE__NOUN_", //"The_ADVERB__ADJECTIVE__NOUN_", //"The_ADVERB__ADJECTIVE__NOUN_s", //"The_ADVERB__ADJECTIVE__PLURALNOUN__VERB_", // WolvesComputeBadly //"_PLURALNOUN__VERB__ADVERB_", // UniteFacilitateAndMerge //"_VERB__VERB_And_VERB_", //NastyWitchesAtThePub //"_ADJECTIVE__PLURALNOUN_AtThe_PLACE_", ]; /* * Returns a random element from the array 'arr' */ function randomElement(arr) { return arr[Math.floor(Math.random() * arr.length)]; } /* * Returns true if the string 's' contains one of the * template strings. */ function hasTemplate(s) { for (var template in CATEGORIES){ if (s.indexOf(template) >= 0){ return true; } } } /** * Generates new room name. */ var RoomNameGenerator = { generateRoomWithoutSeparator: function() { // Note that if more than one pattern is available, the choice of 'name' won't be random (names from patterns // with fewer options will have higher probability of being chosen that names from patterns with more options). var name = randomElement(PATTERNS); var word; while (hasTemplate(name)){ for (var template in CATEGORIES){ word = randomElement(CATEGORIES[template]); name = name.replace(template, word); } } return name; } } module.exports = RoomNameGenerator; },{}],37:[function(require,module,exports){ var animateTimeout, updateTimeout; var RoomNameGenerator = require("./RoomnameGenerator"); function enter_room() { var val = $("#enter_room_field").val(); if(!val) { val = $("#enter_room_field").attr("room_name"); } if (val) { window.location.pathname = "/" + val; } } function animate(word) { var currentVal = $("#enter_room_field").attr("placeholder"); $("#enter_room_field").attr("placeholder", currentVal + word.substr(0, 1)); animateTimeout = setTimeout(function() { animate(word.substring(1, word.length)) }, 70); } function update_roomname() { var word = RoomNameGenerator.generateRoomWithoutSeparator(); $("#enter_room_field").attr("room_name", word); $("#enter_room_field").attr("placeholder", ""); clearTimeout(animateTimeout); animate(word); updateTimeout = setTimeout(update_roomname, 10000); } function setupWelcomePage() { $("#videoconference_page").hide(); $("#domain_name").text( window.location.protocol + "//" + window.location.host + "/"); if (interfaceConfig.SHOW_JITSI_WATERMARK) { var leftWatermarkDiv = $("#welcome_page_header div[class='watermark leftwatermark']"); if(leftWatermarkDiv && leftWatermarkDiv.length > 0) { leftWatermarkDiv.css({display: 'block'}); leftWatermarkDiv.parent().get(0).href = interfaceConfig.JITSI_WATERMARK_LINK; } } if (interfaceConfig.SHOW_BRAND_WATERMARK) { var rightWatermarkDiv = $("#welcome_page_header div[class='watermark rightwatermark']"); if(rightWatermarkDiv && rightWatermarkDiv.length > 0) { rightWatermarkDiv.css({display: 'block'}); rightWatermarkDiv.parent().get(0).href = interfaceConfig.BRAND_WATERMARK_LINK; rightWatermarkDiv.get(0).style.backgroundImage = "url(images/rightwatermark.png)"; } } if (interfaceConfig.SHOW_POWERED_BY) { $("#welcome_page_header>a[class='poweredby']") .css({display: 'block'}); } $("#enter_room_button").click(function() { enter_room(); }); $("#enter_room_field").keydown(function (event) { if (event.keyCode === 13 /* enter */) { enter_room(); } }); if (!(interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE === false)){ var updateTimeout; var animateTimeout; $("#reload_roomname").click(function () { clearTimeout(updateTimeout); clearTimeout(animateTimeout); update_roomname(); }); $("#reload_roomname").show(); update_roomname(); } $("#disable_welcome").click(function () { window.localStorage.welcomePageDisabled = $("#disable_welcome").is(":checked"); }); } module.exports = setupWelcomePage; },{"./RoomnameGenerator":36}],38:[function(require,module,exports){ var params = {}; function getConfigParamsFromUrl() { if(!location.hash) return {}; var hash = location.hash.substr(1); var result = {}; hash.split("&").forEach(function(part) { var item = part.split("="); result[item[0]] = JSON.parse( decodeURIComponent(item[1]).replace(/\\&/, "&")); }); return result; } params = getConfigParamsFromUrl(); var URLProcessor = { setConfigParametersFromUrl: function () { for(var k in params) { if(typeof k !== "string" || k.indexOf("config.") === -1) continue; var v = params[k]; var confKey = k.substr(7); if(config[confKey] && typeof config[confKey] !== typeof v) { console.warn("The type of " + k + " is wrong. That parameter won't be updated in config.js."); continue; } config[confKey] = v; } } }; module.exports = URLProcessor; },{}],39:[function(require,module,exports){ var EventEmitter = require("events"); var eventEmitter = new EventEmitter(); var CQEvents = require("../../service/connectionquality/CQEvents"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); /** * local stats * @type {{}} */ var stats = {}; /** * remote stats * @type {{}} */ 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 statitistics to format used by VideoLayout * @param stats * @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}} */ function parseMUCStats(stats) { return { bitrate: { download: stats.bitrate_download, upload: stats.bitrate_upload }, packetLoss: { total: stats.packetLoss_total, download: stats.packetLoss_download, upload: stats.packetLoss_upload } }; } var ConnectionQuality = { init: function () { APP.xmpp.addListener(XMPPEvents.REMOTE_STATS, this.updateRemoteStats); APP.statistics.addConnectionStatsListener(this.updateLocalStats); APP.statistics.addRemoteStatsStopListener(this.stopSendingStats); }, /** * Updates the local statistics * @param data new statistics */ updateLocalStats: function (data) { stats = data; eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, 100 - stats.packetLoss.total, stats); if (sendIntervalId == null) { startSendingStats(); } }, /** * Updates remote statistics * @param jid the jid associated with the statistics * @param data the statistics */ updateRemoteStats: function (jid, data) { if (data == null || data.packetLoss_total == null) { eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, null, null); return; } remoteStats[jid] = parseMUCStats(data); eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, jid, 100 - data.packetLoss_total, remoteStats[jid]); }, /** * Stops statistics sending. */ stopSendingStats: function () { clearInterval(sendIntervalId); sendIntervalId = null; //notify UI about stopping statistics gathering eventEmitter.emit(CQEvents.STOP); }, /** * Returns the local statistics. */ getStats: function () { return stats; }, addListener: function (type, listener) { eventEmitter.on(type, listener); } }; module.exports = ConnectionQuality; },{"../../service/connectionquality/CQEvents":104,"../../service/xmpp/XMPPEvents":108,"events":1}],40:[function(require,module,exports){ /* global $, alert, APP, changeLocalVideo, chrome, config, getConferenceHandler, getUserMediaWithConstraints */ /** * Indicates that desktop stream is currently in use(for toggle purpose). * @type {boolean} */ var isUsingScreenStream = false; /** * Indicates that switch stream operation is in progress and prevent from * triggering new events. * @type {boolean} */ var switchInProgress = false; /** * Method used to get screen sharing stream. * * @type {function (stream_callback, failure_callback} */ var obtainDesktopStream = null; /** * Indicates whether desktop sharing extension is installed. * @type {boolean} */ var extInstalled = false; /** * Indicates whether update of desktop sharing extension is required. * @type {boolean} */ var extUpdateRequired = false; /** * Flag used to cache desktop sharing enabled state. Do not use directly as * it can be null. * * @type {null|boolean} */ var _desktopSharingEnabled = null; var EventEmitter = require("events"); var eventEmitter = new EventEmitter(); var DesktopSharingEventTypes = require("../../service/desktopsharing/DesktopSharingEventTypes"); /** * Method obtains desktop stream from WebRTC 'screen' source. * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. */ 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() { return "https://chrome.google.com/webstore/detail/" + config.chromeExtensionId; } /** * Checks whether extension update 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 boths version has 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 checkExtInstalled(callback) { if (!chrome.runtime) { // No API, so no extension for sure callback(false, false); return; } chrome.runtime.sendMessage( 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); var updateRequired = isUpdateRequired(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( config.chromeExtensionId, { getStream: true, sources: 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 (extInstalled) { doGetStreamFromExtension(streamCallback, failCallback); } else { if (extUpdateRequired) { 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); extInstalled = 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"); } ); } } /** * Call this method to toggle desktop sharing feature. * @param method pass "ext" to use chrome extension for desktop capture(chrome * extension required), pass "webrtc" to use WebRTC "screen" desktop * source('chrome://flags/#enable-usermedia-screen-capture' must be * enabled), pass any other string or nothing in order to disable this * feature completely. */ function setDesktopSharing(method) { // Check if we are running chrome if (!navigator.webkitGetUserMedia) { obtainDesktopStream = null; console.info("Desktop sharing disabled"); } else if (method == "ext") { obtainDesktopStream = obtainScreenFromExtension; console.info("Using Chrome extension for desktop sharing"); } else if (method == "webrtc") { obtainDesktopStream = obtainWebRTCScreen; console.info("Using Chrome WebRTC for desktop sharing"); } // Reset enabled cache _desktopSharingEnabled = null; } /** * Initializes 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 getVideoStreamFailed(error) { console.error("Failed to obtain the stream to switch to", error); switchInProgress = false; isUsingScreenStream = false; newStreamCreated(null); } function getDesktopStreamFailed(error) { console.error("Failed to obtain the stream to switch to", error); switchInProgress = false; } function streamSwitchDone() { switchInProgress = false; eventEmitter.emit( DesktopSharingEventTypes.SWITCHING_DONE, isUsingScreenStream); } function newStreamCreated(stream) { eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED, stream, isUsingScreenStream, streamSwitchDone); } module.exports = { isUsingScreenStream: function () { return isUsingScreenStream; }, /** * @returns {boolean} true if desktop sharing feature is available * and enabled. */ isDesktopSharingEnabled: function () { if (_desktopSharingEnabled === null) { if (obtainDesktopStream === obtainScreenFromExtension) { // Parse chrome version var userAgent = navigator.userAgent.toLowerCase(); // We can assume that user agent is chrome, because it's // enforced when 'ext' streaming method is set var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10); console.log("Chrome version" + userAgent, ver); _desktopSharingEnabled = ver >= 34; } else { _desktopSharingEnabled = obtainDesktopStream === obtainWebRTCScreen; } } return _desktopSharingEnabled; }, init: function () { setDesktopSharing(config.desktopSharing); // Initialize Chrome extension inline installs if (config.chromeExtensionId) { initInlineInstalls(); // Check if extension is installed checkExtInstalled(function (installed, updateRequired) { extInstalled = installed; extUpdateRequired = updateRequired; console.info( "Chrome extension installed: " + extInstalled + " updateRequired: " + extUpdateRequired); }); } eventEmitter.emit(DesktopSharingEventTypes.INIT); }, addListener: function (listener, type) { eventEmitter.on(type, listener); }, removeListener: function (listener, type) { eventEmitter.removeListener(type, listener); }, /* * Toggles screen sharing. */ toggleScreenSharing: function () { if (switchInProgress || !obtainDesktopStream) { console.warn("Switch in progress or no method defined"); return; } switchInProgress = true; if (!isUsingScreenStream) { // Switch to desktop stream obtainDesktopStream( function (stream) { // We now use screen stream isUsingScreenStream = true; // Hook 'ended' event to restore camera // when screen stream stops stream.addEventListener('ended', function (e) { if (!switchInProgress && isUsingScreenStream) { APP.desktopsharing.toggleScreenSharing(); } } ); newStreamCreated(stream); }, getDesktopStreamFailed); } else { // Disable screen stream APP.RTC.getUserMediaWithConstraints( ['video'], function (stream) { // We are now using camera stream isUsingScreenStream = false; newStreamCreated(stream); }, getVideoStreamFailed, config.resolution || '360' ); } } }; },{"../../service/desktopsharing/DesktopSharingEventTypes":105,"events":1}],41:[function(require,module,exports){ //maps keycode to character, id of popover for given function and function var shortcuts = { 67: { character: "C", id: "toggleChatPopover", function: APP.UI.toggleChat }, 70: { character: "F", id: "filmstripPopover", function: APP.UI.toggleFilmStrip }, 77: { character: "M", id: "mutePopover", function: APP.UI.toggleAudio }, 84: { character: "T", function: function() { if(!APP.RTC.localAudio.isMuted()) { APP.UI.toggleAudio(); } } }, 86: { character: "V", id: "toggleVideoPopover", function: APP.UI.toggleVideo } }; var KeyboardShortcut = { init: function () { window.onkeyup = function(e) { var keycode = e.which; if(!($(":focus").is("input[type=text]") || $(":focus").is("input[type=password]") || $(":focus").is("textarea"))) { if (typeof shortcuts[keycode] === "object") { shortcuts[keycode].function(); } else if (keycode >= "0".charCodeAt(0) && keycode <= "9".charCodeAt(0)) { APP.UI.clickOnVideo(keycode - "0".charCodeAt(0) + 1); } //esc while the smileys are visible hides them } else if (keycode === 27 && $('#smileysContainer').is(':visible')) { APP.UI.toggleSmileys(); } }; window.onkeydown = function(e) { if(!($(":focus").is("input[type=text]") || $(":focus").is("input[type=password]") || $(":focus").is("textarea"))) { if(e.which === "T".charCodeAt(0)) { if(APP.RTC.localAudio.isMuted()) { APP.UI.toggleAudio(); } } } }; var self = this; $('body').popover({ selector: '[data-toggle=popover]', trigger: 'click hover', content: function() { return this.getAttribute("content") + self.getShortcut(this.getAttribute("shortcut")); } }); }, /** * * @param id indicates the popover associated with the shortcut * @returns {string} the keyboard shortcut used for the id given */ getShortcut: function (id) { for (var keycode in shortcuts) { if (shortcuts.hasOwnProperty(keycode)) { if (shortcuts[keycode].id === id) { return " (" + shortcuts[keycode].character + ")"; } } } return ""; } }; module.exports = KeyboardShortcut; },{}],42:[function(require,module,exports){ /* global APP */ /** * 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; },{"../../service/members/Events":106,"../../service/xmpp/XMPPEvents":108,"events":1}],43:[function(require,module,exports){ var email = ''; var displayName = ''; var userId; var language = null; function supportsLocalStorage() { try { return 'localStorage' in window && window.localStorage !== null; } catch (e) { console.log("localstorage is not supported"); return false; } } function generateUniqueId() { function _p8() { return (Math.random().toString(16) + "000000000").substr(2, 8); } return _p8() + _p8() + _p8() + _p8(); } if (supportsLocalStorage()) { if (!window.localStorage.jitsiMeetId) { window.localStorage.jitsiMeetId = generateUniqueId(); console.log("generated id", window.localStorage.jitsiMeetId); } userId = window.localStorage.jitsiMeetId || ''; email = window.localStorage.email || ''; displayName = window.localStorage.displayname || ''; language = window.localStorage.language; } else { console.log("local storage is not supported"); userId = generateUniqueId(); } var Settings = { setDisplayName: function (newDisplayName) { displayName = newDisplayName; window.localStorage.displayname = displayName; return displayName; }, setEmail: function (newEmail) { email = newEmail; window.localStorage.email = newEmail; return email; }, getSettings: function () { return { email: email, displayName: displayName, uid: userId, language: language }; }, setLanguage: function (lang) { language = lang; window.localStorage.language = lang; } }; module.exports = Settings; },{}],44:[function(require,module,exports){ /** * Provides statistics for the local stream. */ /** * Size of the webaudio analizer buffer. * @type {number} */ var WEBAUDIO_ANALIZER_FFT_SIZE = 2048; /** * Value of the webaudio analizer smoothing time parameter. * @type {number} */ var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8; /** * Converts time domain data array to audio level. * @param array 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)); } /** * LocalStatsCollector calculates statistics for the local stream. * * @param stream the local stream * @param interval stats refresh interval given in ms. * @param {function(LocalStatsCollector)} updateCallback the callback called on stats * update. * @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) return; var context = new AudioContext(); var analyser = context.createAnalyser(); analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME; analyser.fftSize = WEBAUDIO_ANALIZER_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( "statistics.audioLevel", 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; },{}],45:[function(require,module,exports){ /* global ssrc2jid */ /* jshint -W117 */ var RTCBrowserType = require("../../service/RTC/RTCBrowserType"); /** * Calculates packet lost percent using the number of lost packets and the * number of all packet. * @param lostPackets the number of lost packets * @param totalPackets the number of all packets. * @returns {number} packet loss percent */ function calculatePacketLoss(lostPackets, totalPackets) { if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0) return 0; return Math.round((lostPackets/totalPackets)*100); } function getStatValue(item, name) { if(!keyMap[APP.RTC.getBrowserType()][name]) throw "The property isn't supported!"; var key = keyMap[APP.RTC.getBrowserType()][name]; return APP.RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_CHROME? item.stat(key) : item[key]; } /** * Peer statistics data holder. * @constructor */ function PeerStats() { this.ssrc2Loss = {}; this.ssrc2AudioLevel = {}; this.ssrc2bitrate = {}; this.ssrc2resolution = {}; } /** * The bandwidth * @type {{}} */ PeerStats.bandwidth = {}; /** * The bit rate * @type {{}} */ PeerStats.bitrate = {}; /** * The packet loss rate * @type {{}} */ PeerStats.packetLoss = null; /** * Sets packets loss rate for given ssrc that blong to the peer * represented by this instance. * @param ssrc audio or video RTP stream SSRC. * @param lossRate new packet loss rate value to be set. */ PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate) { this.ssrc2Loss[ssrc] = lossRate; }; /** * Sets resolution for given ssrc that belong to the peer * represented by this instance. * @param ssrc audio or video RTP stream SSRC. * @param resolution new resolution value to be set. */ PeerStats.prototype.setSsrcResolution = function (ssrc, resolution) { if(resolution === null && this.ssrc2resolution[ssrc]) { delete this.ssrc2resolution[ssrc]; } else if(resolution !== null) this.ssrc2resolution[ssrc] = resolution; }; /** * Sets the bit rate for given ssrc that blong to the peer * represented by this instance. * @param ssrc audio or video RTP stream SSRC. * @param bitrate new bitrate value to be set. */ PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate) { if(this.ssrc2bitrate[ssrc]) { this.ssrc2bitrate[ssrc].download += bitrate.download; this.ssrc2bitrate[ssrc].upload += bitrate.upload; } else { this.ssrc2bitrate[ssrc] = bitrate; } }; /** * Sets new audio level(input or output) for given ssrc that identifies * the stream which belongs to the peer represented by this instance. * @param ssrc RTP stream SSRC for which current audio level value will be * updated. * @param audioLevel the new audio level value to be set. Value is truncated to * fit the range from 0 to 1. */ PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel) { // Range limit 0 - 1 this.ssrc2AudioLevel[ssrc] = formatAudioLevel(audioLevel); }; function formatAudioLevel(audioLevel) { return Math.min(Math.max(audioLevel, 0), 1); } /** * Array with the transport information. * @type {Array} */ PeerStats.transport = []; /** * StatsCollector registers for stats updates of given * peerconnection in given interval. On each update particular * stats are extracted and put in {@link PeerStats} objects. Once the processing * is done audioLevelsUpdateCallback is called with this * instance as an event source. * * @param peerconnection webRTC peer connection object. * @param interval stats refresh interval given in ms. * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback * called on stats update. * @constructor */ function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter) { this.peerconnection = peerconnection; this.baselineAudioLevelsReport = null; this.currentAudioLevelsReport = null; this.currentStatsReport = null; this.baselineStatsReport = null; this.audioLevelsIntervalId = null; this.eventEmitter = eventEmitter; /** * Gather PeerConnection stats once every this many milliseconds. */ this.GATHER_INTERVAL = 15000; /** * Log stats via the focus once every this many milliseconds. */ this.LOG_INTERVAL = 60000; /** * Gather stats and store them in this.statsToBeLogged. */ this.gatherStatsIntervalId = null; /** * Send the stats already saved in this.statsToBeLogged to be logged via * the focus. */ this.logStatsIntervalId = null; /** * Stores the statistics which will be send to the focus to be logged. */ this.statsToBeLogged = { timestamps: [], stats: {} }; // Updates stats interval this.audioLevelsIntervalMilis = audioLevelsInterval; this.statsIntervalId = null; this.statsIntervalMilis = statsInterval; // Map of jids to PeerStats this.jid2stats = {}; } module.exports = StatsCollector; /** * Stops stats updates. */ StatsCollector.prototype.stop = function () { if (this.audioLevelsIntervalId) { clearInterval(this.audioLevelsIntervalId); this.audioLevelsIntervalId = null; } if (this.statsIntervalId) { clearInterval(this.statsIntervalId); this.statsIntervalId = null; } if(this.logStatsIntervalId) { clearInterval(this.logStatsIntervalId); this.logStatsIntervalId = null; } if(this.gatherStatsIntervalId) { clearInterval(this.gatherStatsIntervalId); this.gatherStatsIntervalId = null; } }; /** * Callback passed to getStats method. * @param error an error that occurred on getStats call. */ StatsCollector.prototype.errorCallback = function (error) { console.error("Get stats error", error); this.stop(); }; /** * Starts stats updates. */ StatsCollector.prototype.start = function () { var self = this; if(!config.disableAudioLevels) { this.audioLevelsIntervalId = setInterval( function () { // Interval updates self.peerconnection.getStats( function (report) { var results = null; if (!report || !report.result || typeof report.result != 'function') { results = report; } else { results = report.result(); } //console.error("Got interval report", results); self.currentAudioLevelsReport = results; self.processAudioLevelReport(); self.baselineAudioLevelsReport = self.currentAudioLevelsReport; }, self.errorCallback ); }, self.audioLevelsIntervalMilis ); } if(!config.disableStats && !navigator.mozGetUserMedia) { this.statsIntervalId = setInterval( function () { // Interval updates self.peerconnection.getStats( function (report) { var results = null; if (!report || !report.result || typeof report.result != 'function') { //firefox results = report; } else { //chrome results = report.result(); } //console.error("Got interval report", results); self.currentStatsReport = results; try { self.processStatsReport(); } catch (e) { console.error("Unsupported key:" + e, e); } self.baselineStatsReport = self.currentStatsReport; }, self.errorCallback ); }, self.statsIntervalMilis ); } if (config.logStats && !navigator.mozGetUserMedia) { this.gatherStatsIntervalId = setInterval( function () { self.peerconnection.getStats( function (report) { self.addStatsToBeLogged(report.result()); }, function () { } ); }, this.GATHER_INTERVAL ); this.logStatsIntervalId = setInterval( function() { self.logStats(); }, this.LOG_INTERVAL); } }; /** * Checks whether a certain record should be included in the logged statistics. */ function acceptStat(reportId, reportType, statName) { if (reportType == "googCandidatePair" && statName == "googChannelId") return false; if (reportType == "ssrc") { if (statName == "googTrackId" || statName == "transportId" || statName == "ssrc") return false; } return true; } /** * Checks whether a certain record should be included in the logged statistics. */ function acceptReport(id, type) { if (id.substring(0, 15) == "googCertificate" || id.substring(0, 9) == "googTrack" || id.substring(0, 20) == "googLibjingleSession") return false; if (type == "googComponent") return false; return true; } /** * Converts the stats to the format used for logging, and saves the data in * this.statsToBeLogged. * @param reports Reports as given by webkitRTCPerConnection.getStats. */ StatsCollector.prototype.addStatsToBeLogged = function (reports) { var self = this; var num_records = this.statsToBeLogged.timestamps.length; this.statsToBeLogged.timestamps.push(new Date().getTime()); reports.map(function (report) { if (!acceptReport(report.id, report.type)) return; var stat = self.statsToBeLogged.stats[report.id]; if (!stat) { stat = self.statsToBeLogged.stats[report.id] = {}; } stat.type = report.type; report.names().map(function (name) { if (!acceptStat(report.id, report.type, name)) return; var values = stat[name]; if (!values) { values = stat[name] = []; } while (values.length < num_records) { values.push(null); } values.push(report.stat(name)); }); }); }; StatsCollector.prototype.logStats = function () { if(!APP.xmpp.sendLogs(this.statsToBeLogged)) return; // Reset the stats this.statsToBeLogged.stats = {}; this.statsToBeLogged.timestamps = []; }; var keyMap = {}; keyMap[RTCBrowserType.RTC_BROWSER_FIREFOX] = { "ssrc": "ssrc", "packetsReceived": "packetsReceived", "packetsLost": "packetsLost", "packetsSent": "packetsSent", "bytesReceived": "bytesReceived", "bytesSent": "bytesSent" }; keyMap[RTCBrowserType.RTC_BROWSER_CHROME] = { "receiveBandwidth": "googAvailableReceiveBandwidth", "sendBandwidth": "googAvailableSendBandwidth", "remoteAddress": "googRemoteAddress", "transportType": "googTransportType", "localAddress": "googLocalAddress", "activeConnection": "googActiveConnection", "ssrc": "ssrc", "packetsReceived": "packetsReceived", "packetsSent": "packetsSent", "packetsLost": "packetsLost", "bytesReceived": "bytesReceived", "bytesSent": "bytesSent", "googFrameHeightReceived": "googFrameHeightReceived", "googFrameWidthReceived": "googFrameWidthReceived", "googFrameHeightSent": "googFrameHeightSent", "googFrameWidthSent": "googFrameWidthSent", "audioInputLevel": "audioInputLevel", "audioOutputLevel": "audioOutputLevel" }; /** * Stats processing logic. */ StatsCollector.prototype.processStatsReport = function () { if (!this.baselineStatsReport) { return; } for (var idx in this.currentStatsReport) { var now = this.currentStatsReport[idx]; try { if (getStatValue(now, 'receiveBandwidth') || getStatValue(now, 'sendBandwidth')) { PeerStats.bandwidth = { "download": Math.round( (getStatValue(now, 'receiveBandwidth')) / 1000), "upload": Math.round( (getStatValue(now, 'sendBandwidth')) / 1000) }; } } catch(e){/*not supported*/} if(now.type == 'googCandidatePair') { var ip, type, localIP, active; try { ip = getStatValue(now, 'remoteAddress'); type = getStatValue(now, "transportType"); localIP = getStatValue(now, "localAddress"); active = getStatValue(now, "activeConnection"); } catch(e){/*not supported*/} if(!ip || !type || !localIP || active != "true") continue; var addressSaved = false; for(var i = 0; i < PeerStats.transport.length; i++) { if(PeerStats.transport[i].ip == ip && PeerStats.transport[i].type == type && PeerStats.transport[i].localip == localIP) { addressSaved = true; } } if(addressSaved) continue; PeerStats.transport.push({localip: localIP, ip: ip, type: type}); continue; } if(now.type == "candidatepair") { if(now.state == "succeeded") continue; var local = this.currentStatsReport[now.localCandidateId]; var remote = this.currentStatsReport[now.remoteCandidateId]; PeerStats.transport.push({localip: local.ipAddress + ":" + local.portNumber, ip: remote.ipAddress + ":" + remote.portNumber, type: local.transport}); } if (now.type != 'ssrc' && now.type != "outboundrtp" && now.type != "inboundrtp") { continue; } var before = this.baselineStatsReport[idx]; if (!before) { console.warn(getStatValue(now, 'ssrc') + ' not enough data'); continue; } var ssrc = getStatValue(now, 'ssrc'); if(!ssrc) continue; var jid = APP.xmpp.getJidFromSSRC(ssrc); if (!jid && (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; } var isDownloadStream = true; var key = 'packetsReceived'; if (!getStatValue(now, key)) { isDownloadStream = false; key = 'packetsSent'; if (!getStatValue(now, key)) { console.warn("No packetsReceived nor packetSent stat found"); continue; } } var packetsNow = getStatValue(now, key); if(!packetsNow || packetsNow < 0) packetsNow = 0; var packetsBefore = getStatValue(before, key); if(!packetsBefore || packetsBefore < 0) packetsBefore = 0; var packetRate = packetsNow - packetsBefore; if(!packetRate || packetRate < 0) packetRate = 0; var currentLoss = getStatValue(now, 'packetsLost'); if(!currentLoss || currentLoss < 0) currentLoss = 0; var previousLoss = getStatValue(before, 'packetsLost'); if(!previousLoss || previousLoss < 0) previousLoss = 0; var lossRate = currentLoss - previousLoss; if(!lossRate || lossRate < 0) lossRate = 0; var packetsTotal = (packetRate + lossRate); jidStats.setSsrcLoss(ssrc, {"packetsTotal": packetsTotal, "packetsLost": lossRate, "isDownloadStream": isDownloadStream}); var bytesReceived = 0, bytesSent = 0; if(getStatValue(now, "bytesReceived")) { bytesReceived = getStatValue(now, "bytesReceived") - getStatValue(before, "bytesReceived"); } if(getStatValue(now, "bytesSent")) { bytesSent = getStatValue(now, "bytesSent") - getStatValue(before, "bytesSent"); } var time = Math.round((now.timestamp - before.timestamp) / 1000); if(bytesReceived <= 0 || time <= 0) { bytesReceived = 0; } else { bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000); } if(bytesSent <= 0 || time <= 0) { bytesSent = 0; } else { bytesSent = Math.round(((bytesSent * 8) / time) / 1000); } jidStats.setSsrcBitrate(ssrc, { "download": bytesReceived, "upload": bytesSent}); var resolution = {height: null, width: null}; try { if (getStatValue(now, "googFrameHeightReceived") && getStatValue(now, "googFrameWidthReceived")) { resolution.height = getStatValue(now, "googFrameHeightReceived"); resolution.width = getStatValue(now, "googFrameWidthReceived"); } else if (getStatValue(now, "googFrameHeightSent") && getStatValue(now, "googFrameWidthSent")) { resolution.height = getStatValue(now, "googFrameHeightSent"); resolution.width = getStatValue(now, "googFrameWidthSent"); } } catch(e){/*not supported*/} if(resolution.height && resolution.width) { jidStats.setSsrcResolution(ssrc, resolution); } else { jidStats.setSsrcResolution(ssrc, null); } } var self = this; // Jid stats var totalPackets = {download: 0, upload: 0}; var lostPackets = {download: 0, upload: 0}; var bitrateDownload = 0; var bitrateUpload = 0; var resolutions = {}; Object.keys(this.jid2stats).forEach( function (jid) { Object.keys(self.jid2stats[jid].ssrc2Loss).forEach( function (ssrc) { var type = "upload"; if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream) type = "download"; totalPackets[type] += self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal; lostPackets[type] += self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost; } ); Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach( function (ssrc) { bitrateDownload += self.jid2stats[jid].ssrc2bitrate[ssrc].download; bitrateUpload += self.jid2stats[jid].ssrc2bitrate[ssrc].upload; delete self.jid2stats[jid].ssrc2bitrate[ssrc]; } ); resolutions[jid] = self.jid2stats[jid].ssrc2resolution; } ); PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload}; PeerStats.packetLoss = { total: calculatePacketLoss(lostPackets.download + lostPackets.upload, totalPackets.download + totalPackets.upload), download: calculatePacketLoss(lostPackets.download, totalPackets.download), upload: calculatePacketLoss(lostPackets.upload, totalPackets.upload) }; this.eventEmitter.emit("statistics.connectionstats", { "bitrate": PeerStats.bitrate, "packetLoss": PeerStats.packetLoss, "bandwidth": PeerStats.bandwidth, "resolution": resolutions, "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("statistics.audioLevel", jid, audioLevel); } } }; },{"../../service/RTC/RTCBrowserType":98}],46:[function(require,module,exports){ /** * 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 eventEmitter = new EventEmitter(); var localStats = null; var rtpStats = null; function stopLocal() { if(localStats) { localStats.stop(); localStats = null; } } function stopRemote() { if(rtpStats) { rtpStats.stop(); eventEmitter.emit("statistics.stop"); rtpStats = null; } } function startRemoteStats (peerconnection) { if(rtpStats) { rtpStats.stop(); rtpStats = null; } rtpStats = new RTPStats(peerconnection, 200, 2000, eventEmitter); 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) { stopRemote(); if(onUnload) { stopLocal(); eventEmitter.removeAllListeners(); } } var statistics = { /** * Indicates that this audio level is for local jid. * @type {string} */ LOCAL_JID: 'local', addAudioLevelListener: function(listener) { eventEmitter.on("statistics.audioLevel", listener); }, removeAudioLevelListener: function(listener) { eventEmitter.removeListener("statistics.audioLevel", listener); }, addConnectionStatsListener: function(listener) { eventEmitter.on("statistics.connectionstats", listener); }, removeConnectionStatsListener: function(listener) { eventEmitter.removeListener("statistics.connectionstats", listener); }, addRemoteStatsStopListener: function(listener) { eventEmitter.on("statistics.stop", listener); }, removeRemoteStatsStopListener: function(listener) { eventEmitter.removeListener("statistics.stop", listener); }, stop: function () { stopLocal(); stopRemote(); 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); APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) { startRemoteStats(event.peerconnection); }); } }; module.exports = statistics; },{"../../service/RTC/StreamEventTypes.js":101,"../../service/xmpp/XMPPEvents":108,"./LocalStatsCollector.js":44,"./RTPStatsCollector.js":45,"events":1}],47:[function(require,module,exports){ 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) { return value.replace("__app__", interfaceConfig.APP_NAME); }); var defaultOptions = { detectLngQS: "lang", useCookie: false, fallbackLng: DEFAULT_LANG, load: "unspecific", resGetPath: 'lang/__ns__-__lng__.json', ns: { namespaces: ['main', 'languages'], defaultNs: 'main' }, lngWhitelist : languages.getLanguages(), fallbackOnNull: true, fallbackOnEmpty: true, useDataAttrOptions: true, defaultValueFromContent: false, app: interfaceConfig.APP_NAME, getAsync: false, defaultValueFromContent: false, customLoad: function(lng, ns, options, done) { var resPath = "lang/__ns__-__lng__.json"; if(lng === languages.EN) resPath = "lang/__ns__.json"; var url = i18n.functions.applyReplacement(resPath, { lng: lng, ns: ns }); i18n.functions.ajax({ url: url, success: function(data, status, xhr) { i18n.functions.log('loaded: ' + url); done(null, data); }, error : function(xhr, status, error) { if ((status && status == 200) || (xhr && xhr.status && xhr.status == 200)) { // file loaded but invalid json, stop waste time ! i18n.functions.error('There is a typo in: ' + url); } else if ((status && status == 404) || (xhr && xhr.status && xhr.status == 404)) { i18n.functions.log('Does not exist: ' + url); } else { var theStatus = status ? status : ((xhr && xhr.status) ? xhr.status : null); i18n.functions.log(theStatus + ' when loading ' + url); } done(error, {}); }, dataType: "json", async : options.getAsync }); } // options for caching // useLocalStorage: true, // localStorageExpirationTime: 86400000 // in ms, default 1 week }; function initCompleted(t) { $("[data-i18n]").i18n(); } function checkForParameter() { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i 0) { var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session); ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder', name: (cands[0].sdpMid? cands[0].sdpMid : mline.media) }).c('transport', ice); for (var i = 0; i < cands.length; i++) { cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up(); } // add fingerprint if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) { var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)); tmp.required = true; cand.c( 'fingerprint', {xmlns: 'urn:xmpp:jingle:apps:dtls:0'}) .t(tmp.fingerprint); delete tmp.fingerprint; cand.attrs(tmp); cand.up(); } cand.up(); // transport cand.up(); // content } } // might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340 //console.log('was this the last candidate', this.lasticecandidate); this.connection.sendIQ(cand, function () { var ack = {}; ack.source = 'transportinfo'; $(document).trigger('ack.jingle', [this.sid, ack]); }, function (stanza) { var error = ($(stanza).find('error').length) ? { code: $(stanza).find('error').attr('code'), reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'transportinfo'; JingleSession.onJingleError(this.sid, error); }, 10000); }; JingleSession.prototype.sendOffer = function () { //console.log('sendOffer...'); var self = this; this.peerconnection.createOffer(function (sdp) { self.createdOffer(sdp); }, function (e) { console.error('createOffer failed', e); }, this.media_constraints ); }; JingleSession.prototype.createdOffer = function (sdp) { //console.log('createdOffer', sdp); var self = this; this.localSDP = new SDP(sdp.sdp); //this.localSDP.mangle(); var sendJingle = function () { var init = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', action: 'session-initiate', initiator: this.initiator, sid: this.sid}); self.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC); self.connection.sendIQ(init, function () { var ack = {}; ack.source = 'offer'; $(document).trigger('ack.jingle', [self.sid, ack]); }, function (stanza) { self.state = 'error'; self.peerconnection.close(); var error = ($(stanza).find('error').length) ? { code: $(stanza).find('error').attr('code'), reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'offer'; JingleSession.onJingleError(self.sid, error); }, 10000); } sdp.sdp = this.localSDP.raw; this.peerconnection.setLocalDescription(sdp, function () { if(self.usetrickle) { sendJingle(); } self.setLocalDescription(); //console.log('setLocalDescription success'); }, function (e) { console.error('setLocalDescription failed', e); } ); var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:'); for (var i = 0; i < cands.length; i++) { var cand = SDPUtil.parse_icecandidate(cands[i]); if (cand.type == 'srflx') { this.hadstuncandidate = true; } else if (cand.type == 'relay') { this.hadturncandidate = true; } } }; JingleSession.prototype.setRemoteDescription = function (elem, desctype) { //console.log('setting remote description... ', desctype); this.remoteSDP = new SDP(''); this.remoteSDP.fromJingle(elem); if (this.peerconnection.remoteDescription !== null) { console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription); if (this.peerconnection.remoteDescription.type == 'pranswer') { var pranswer = new SDP(this.peerconnection.remoteDescription.sdp); for (var i = 0; i < pranswer.media.length; i++) { // make sure we have ice ufrag and pwd if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) { if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) { this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n'; } else { console.warn('no ice ufrag?'); } if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) { this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n'; } else { console.warn('no ice pwd?'); } } // copy over candidates var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:'); for (var j = 0; j < lines.length; j++) { this.remoteSDP.media[i] += lines[j] + '\r\n'; } } this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join(''); } } var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw}); this.peerconnection.setRemoteDescription(remotedesc, function () { //console.log('setRemoteDescription success'); }, function (e) { console.error('setRemoteDescription error', e); JingleSession.onJingleFatalError(self, e); } ); }; JingleSession.prototype.addIceCandidate = function (elem) { var self = this; if (this.peerconnection.signalingState == 'closed') { return; } if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') { console.log('trickle ice candidate arriving before session accept...'); // create a PRANSWER for setRemoteDescription if (!this.remoteSDP) { var cobbled = 'v=0\r\n' + 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME 's=-\r\n' + 't=0 0\r\n'; // first, take some things from the local description for (var i = 0; i < this.localSDP.media.length; i++) { cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n'; cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n'; if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) { cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n'; } cobbled += 'a=inactive\r\n'; } this.remoteSDP = new SDP(cobbled); } // then add things like ice and dtls from remote candidate elem.each(function () { for (var i = 0; i < self.remoteSDP.media.length; i++) { if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) || self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) { var tmp = $(this).find('transport'); self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n'; self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n'; tmp = $(this).find('transport>fingerprint'); if (tmp.length) { self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n'; } else { console.log('no dtls fingerprint (webrtc issue #1718?)'); self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n'; } break; } } } }); this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join(''); // we need a complete SDP with ice-ufrag/ice-pwd in all parts // this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts // but it could be in the session part as well. since the code above constructs this sdp this can't happen however var iscomplete = this.remoteSDP.media.filter(function (mediapart) { return SDPUtil.find_line(mediapart, 'a=ice-ufrag:'); }).length == this.remoteSDP.media.length; if (iscomplete) { console.log('setting pranswer'); try { this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }), function() { }, function(e) { console.log('setRemoteDescription pranswer failed', e.toString()); }); } catch (e) { console.error('setting pranswer failed', e); } } else { //console.log('not yet setting pranswer'); } } // operate on each content element elem.each(function () { // would love to deactivate this, but firefox still requires it var idx = -1; var i; for (i = 0; i < self.remoteSDP.media.length; i++) { if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) || self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { idx = i; break; } } if (idx == -1) { // fall back to localdescription for (i = 0; i < self.localSDP.media.length; i++) { if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) || self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) { idx = i; break; } } } var name = $(this).attr('name'); // TODO: check ice-pwd and ice-ufrag? $(this).find('transport>candidate').each(function () { var line, candidate; line = SDPUtil.candidateFromJingle(this); candidate = new RTCIceCandidate({sdpMLineIndex: idx, sdpMid: name, candidate: line}); try { self.peerconnection.addIceCandidate(candidate); } catch (e) { console.error('addIceCandidate failed', e.toString(), line); } }); }); }; JingleSession.prototype.sendAnswer = function (provisional) { //console.log('createAnswer', provisional); var self = this; this.peerconnection.createAnswer( function (sdp) { self.createdAnswer(sdp, provisional); }, function (e) { console.error('createAnswer failed', e); }, this.media_constraints ); }; JingleSession.prototype.createdAnswer = function (sdp, provisional) { //console.log('createAnswer callback'); var self = this; this.localSDP = new SDP(sdp.sdp); //this.localSDP.mangle(); this.usepranswer = provisional === true; if (this.usetrickle) { if (this.usepranswer) { sdp.type = 'pranswer'; for (var i = 0; i < this.localSDP.media.length; i++) { this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n'); } this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join(''); } } var self = this; var sendJingle = function (ssrcs) { var accept = $iq({to: self.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', action: 'session-accept', initiator: self.initiator, responder: self.responder, sid: self.sid }); self.localSDP.toJingle(accept, self.initiator == self.me ? 'initiator' : 'responder', ssrcs); self.connection.sendIQ(accept, function () { var ack = {}; ack.source = 'answer'; $(document).trigger('ack.jingle', [self.sid, ack]); }, function (stanza) { var error = ($(stanza).find('error').length) ? { code: $(stanza).find('error').attr('code'), reason: $(stanza).find('error :first')[0].tagName, }:{}; error.source = 'answer'; JingleSession.onJingleError(self.sid, error); }, 10000); } sdp.sdp = this.localSDP.raw; this.peerconnection.setLocalDescription(sdp, function () { //console.log('setLocalDescription success'); if (self.usetrickle && !self.usepranswer) { sendJingle(); } self.setLocalDescription(); }, function (e) { console.error('setLocalDescription failed', e); } ); var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:'); for (var j = 0; j < cands.length; j++) { var cand = SDPUtil.parse_icecandidate(cands[j]); if (cand.type == 'srflx') { this.hadstuncandidate = true; } else if (cand.type == 'relay') { this.hadturncandidate = true; } } }; JingleSession.prototype.sendTerminate = function (reason, text) { var self = this, term = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', action: 'session-terminate', initiator: this.initiator, sid: this.sid}) .c('reason') .c(reason || 'success'); if (text) { term.up().c('text').t(text); } this.connection.sendIQ(term, function () { self.peerconnection.close(); self.peerconnection = null; self.terminate(); var ack = {}; ack.source = 'terminate'; $(document).trigger('ack.jingle', [self.sid, ack]); }, function (stanza) { var error = ($(stanza).find('error').length) ? { code: $(stanza).find('error').attr('code'), reason: $(stanza).find('error :first')[0].tagName, }:{}; $(document).trigger('ack.jingle', [self.sid, error]); }, 10000); if (this.statsinterval !== null) { window.clearInterval(this.statsinterval); this.statsinterval = null; } }; JingleSession.prototype.addSource = function (elem, fromJid) { var self = this; // FIXME: dirty waiting if (!this.peerconnection.localDescription) { console.warn("addSource - localDescription not ready yet") setTimeout(function() { self.addSource(elem, fromJid); }, 200 ); return; } console.log('addssrc', new Date().getTime()); console.log('ice', this.peerconnection.iceConnectionState); var sdp = new SDP(this.peerconnection.remoteDescription.sdp); var mySdp = new SDP(this.peerconnection.localDescription.sdp); $(elem).each(function (idx, content) { var name = $(content).attr('name'); var lines = ''; $(content).find('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 != 0) { lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; } }); var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source tmp.each(function () { var ssrc = $(this).attr('ssrc'); if(mySdp.containsSSRC(ssrc)){ /** * This happens when multiple participants change their streams at the same time and * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple * addssrc are scheduled for update IQ. See */ console.warn("Got add stream request for my own ssrc: "+ssrc); return; } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) lines += ':' + $(this).attr('value'); lines += '\r\n'; }); }); sdp.media.forEach(function(media, idx) { if (!SDPUtil.find_line(media, 'a=mid:' + name)) return; sdp.media[idx] += lines; if (!self.addssrc[idx]) self.addssrc[idx] = ''; self.addssrc[idx] += lines; }); sdp.raw = sdp.session + sdp.media.join(''); }); this.modifySourcesQueue.push(function() { // When a source is added and if this is FF, a new channel is allocated // for receiving the added source. We need to diffuse the SSRC of this // new recvonly channel to the rest of the peers. console.log('modify sources done'); var newSdp = new SDP(self.peerconnection.localDescription.sdp); console.log("SDPs", mySdp, newSdp); self.notifyMySSRCUpdate(mySdp, newSdp); }); }; JingleSession.prototype.removeSource = function (elem, fromJid) { var self = this; // FIXME: dirty waiting if (!this.peerconnection.localDescription) { console.warn("removeSource - localDescription not ready yet") setTimeout(function() { self.removeSource(elem, fromJid); }, 200 ); return; } console.log('removessrc', new Date().getTime()); console.log('ice', this.peerconnection.iceConnectionState); var sdp = new SDP(this.peerconnection.remoteDescription.sdp); var mySdp = new SDP(this.peerconnection.localDescription.sdp); $(elem).each(function (idx, content) { var name = $(content).attr('name'); var lines = ''; $(content).find('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 != 0) { lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; } }); var tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source tmp.each(function () { var ssrc = $(this).attr('ssrc'); // This should never happen, but can be useful for bug detection if(mySdp.containsSSRC(ssrc)){ console.error("Got remove stream request for my own ssrc: "+ssrc); return; } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) lines += ':' + $(this).attr('value'); lines += '\r\n'; }); }); sdp.media.forEach(function(media, idx) { if (!SDPUtil.find_line(media, 'a=mid:' + name)) return; sdp.media[idx] += lines; if (!self.removessrc[idx]) self.removessrc[idx] = ''; self.removessrc[idx] += lines; }); sdp.raw = sdp.session + sdp.media.join(''); }); this.modifySourcesQueue.push(function() { // When a source is removed and if this is FF, the recvonly channel that // receives the remote stream is deactivated . We need to diffuse the // recvonly SSRC removal to the rest of the peers. console.log('modify sources done'); var newSdp = new SDP(self.peerconnection.localDescription.sdp); console.log("SDPs", mySdp, newSdp); self.notifyMySSRCUpdate(mySdp, newSdp); }); }; JingleSession.prototype._modifySources = function (successCallback, queueCallback) { var self = this; if (this.peerconnection.signalingState == 'closed') return; if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){ // There is nothing to do since scheduled job might have been executed by another succeeding call this.setLocalDescription(); if(successCallback){ successCallback(); } queueCallback(); return; } // Reset switch streams flag this.switchstreams = false; var sdp = new SDP(this.peerconnection.remoteDescription.sdp); // add sources this.addssrc.forEach(function(lines, idx) { sdp.media[idx] += lines; }); this.addssrc = []; // remove sources this.removessrc.forEach(function(lines, idx) { lines = lines.split('\r\n'); lines.pop(); // remove empty last element; lines.forEach(function(line) { sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', ''); }); }); this.removessrc = []; // FIXME: // this was a hack for the situation when only one peer exists // in the conference. // check if still required and remove if (sdp.media[0]) sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv'); if (sdp.media[1]) sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); sdp.raw = sdp.session + sdp.media.join(''); this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}), function() { if(self.signalingState == 'closed') { console.error("createAnswer attempt on closed state"); queueCallback("createAnswer attempt on closed state"); return; } self.peerconnection.createAnswer( function(modifiedAnswer) { // change video direction, see https://github.com/jitsi/jitmeet/issues/41 if (self.pendingop !== null) { var sdp = new SDP(modifiedAnswer.sdp); if (sdp.media.length > 1) { switch(self.pendingop) { case 'mute': sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly'); break; case 'unmute': sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); break; } sdp.raw = sdp.session + sdp.media.join(''); modifiedAnswer.sdp = sdp.raw; } self.pendingop = null; } // FIXME: pushing down an answer while ice connection state // is still checking is bad... //console.log(self.peerconnection.iceConnectionState); // trying to work around another chrome bug //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass'); self.peerconnection.setLocalDescription(modifiedAnswer, function() { //console.log('modified setLocalDescription ok'); self.setLocalDescription(); if(successCallback){ successCallback(); } queueCallback(); }, function(error) { console.error('modified setLocalDescription failed', error); queueCallback(error); } ); }, function(error) { console.error('modified answer failed', error); queueCallback(error); } ); }, function(error) { console.error('modify failed', error); queueCallback(error); } ); }; /** * Switches video streams. * @param new_stream new stream that will be used as video of this session. * @param oldStream old video stream of this session. * @param success_callback callback executed after successful stream switch. */ JingleSession.prototype.switchStreams = function (new_stream, oldStream, success_callback, isAudio) { var self = this; // Remember SDP to figure out added/removed SSRCs var oldSdp = null; if(self.peerconnection) { if(self.peerconnection.localDescription) { oldSdp = new SDP(self.peerconnection.localDescription.sdp); } self.peerconnection.removeStream(oldStream, true); if(new_stream) self.peerconnection.addStream(new_stream); } if(!isAudio) APP.RTC.switchVideoStreams(new_stream, oldStream); // Conference is not active if(!oldSdp || !self.peerconnection) { success_callback(); return; } self.switchstreams = true; self.modifySourcesQueue.push(function() { console.log('modify sources done'); success_callback(); var newSdp = new SDP(self.peerconnection.localDescription.sdp); console.log("SDPs", oldSdp, newSdp); self.notifyMySSRCUpdate(oldSdp, newSdp); }); }; /** * Figures out added/removed ssrcs and send update IQs. * @param old_sdp SDP object for old description. * @param new_sdp SDP object for new description. */ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){ console.log("Too early to send updates"); return; } // send source-remove IQ. sdpDiffer = new SDPDiffer(new_sdp, old_sdp); var remove = $iq({to: this.peerjid, type: 'set'}) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'source-remove', initiator: this.initiator, sid: this.sid } ); var removed = sdpDiffer.toJingle(remove); if (removed) { this.connection.sendIQ(remove, function (res) { console.info('got remove result', res); }, function (err) { console.error('got remove error', err); } ); } else { console.log('removal not necessary'); } // send source-add IQ. var sdpDiffer = new SDPDiffer(old_sdp, new_sdp); var add = $iq({to: this.peerjid, type: 'set'}) .c('jingle', { xmlns: 'urn:xmpp:jingle:1', action: 'source-add', initiator: this.initiator, sid: this.sid } ); var added = sdpDiffer.toJingle(add); if (added) { this.connection.sendIQ(add, function (res) { console.info('got add result', res); }, function (err) { console.error('got add error', err); } ); } else { console.log('addition not necessary'); } }; /** * Mutes/unmutes the (local) video i.e. enables/disables all video tracks. * * @param mute true to mute the (local) video i.e. to disable all video * tracks; otherwise, false * @param callback a function to be invoked with mute after all video * tracks have been enabled/disabled. The function may, optionally, return * another function which is to be invoked after the whole mute/unmute operation * has completed successfully. * @param options an object which specifies optional arguments such as the * boolean key byUser with default value true which * specifies whether the method was initiated in response to a user command (in * contrast to an automatic decision made by the application logic) */ JingleSession.prototype.setVideoMute = function (mute, callback, options) { var byUser; if (options) { byUser = options.byUser; if (typeof byUser === 'undefined') { byUser = true; } } else { byUser = true; } // The user's command to mute the (local) video takes precedence over any // automatic decision made by the application logic. if (byUser) { this.videoMuteByUser = mute; } else if (this.videoMuteByUser) { return; } this.hardMuteVideo(mute); var self = this; var oldSdp = null; if(self.peerconnection) { if(self.peerconnection.localDescription) { oldSdp = new SDP(self.peerconnection.localDescription.sdp); } } this.modifySourcesQueue.push(function() { console.log('modify sources done'); callback(mute); var newSdp = new SDP(self.peerconnection.localDescription.sdp); console.log("SDPs", oldSdp, newSdp); self.notifyMySSRCUpdate(oldSdp, newSdp); }); }; JingleSession.prototype.hardMuteVideo = function (muted) { this.pendingop = muted ? 'mute' : 'unmute'; }; JingleSession.prototype.sendMute = function (muted, content) { var info = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', action: 'session-info', initiator: this.initiator, sid: this.sid }); info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'}); info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'}); if (content) { info.attrs({'name': content}); } this.connection.send(info); }; JingleSession.prototype.sendRinging = function () { var info = $iq({to: this.peerjid, type: 'set'}) .c('jingle', {xmlns: 'urn:xmpp:jingle:1', action: 'session-info', initiator: this.initiator, sid: this.sid }); info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'}); this.connection.send(info); }; JingleSession.prototype.getStats = function (interval) { var self = this; var recv = {audio: 0, video: 0}; var lost = {audio: 0, video: 0}; var lastrecv = {audio: 0, video: 0}; var lastlost = {audio: 0, video: 0}; var loss = {audio: 0, video: 0}; var delta = {audio: 0, video: 0}; this.statsinterval = window.setInterval(function () { if (self && self.peerconnection && self.peerconnection.getStats) { self.peerconnection.getStats(function (stats) { var results = stats.result(); // TODO: there are so much statistics you can get from this.. for (var i = 0; i < results.length; ++i) { if (results[i].type == 'ssrc') { var packetsrecv = results[i].stat('packetsReceived'); var packetslost = results[i].stat('packetsLost'); if (packetsrecv && packetslost) { packetsrecv = parseInt(packetsrecv, 10); packetslost = parseInt(packetslost, 10); if (results[i].stat('googFrameRateReceived')) { lastlost.video = lost.video; lastrecv.video = recv.video; recv.video = packetsrecv; lost.video = packetslost; } else { lastlost.audio = lost.audio; lastrecv.audio = recv.audio; recv.audio = packetsrecv; lost.audio = packetslost; } } } } delta.audio = recv.audio - lastrecv.audio; delta.video = recv.video - lastrecv.video; loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0; loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0; $(document).trigger('packetloss.jingle', [self.sid, loss]); }); } }, interval || 3000); return this.statsinterval; }; JingleSession.onJingleError = function (session, error) { console.error("Jingle error", error); } JingleSession.onJingleFatalError = function (session, error) { this.service.sessionTerminated = true; this.connection.emuc.doLeave(); APP.UI.messageHandler.showError("dialog.sorry", "dialog.internalError"); } JingleSession.prototype.setLocalDescription = function () { // put our ssrcs into presence so other clients can identify our stream var newssrcs = []; var session = transform.parse(this.peerconnection.localDescription.sdp); session.media.forEach(function (media) { if (media.ssrcs != null && media.ssrcs.length > 0) { // TODO(gp) maybe exclude FID streams? media.ssrcs.forEach(function (ssrc) { if (ssrc.attribute !== 'cname') { return; } newssrcs.push({ 'ssrc': ssrc.id, 'type': media.type, 'direction': media.direction }); }); } else if(this.localStreamsSSRC && this.localStreamsSSRC[media.type]) { newssrcs.push({ 'ssrc': this.localStreamsSSRC[media.type], 'type': media.type, 'direction': media.direction }); } }); console.log('new ssrcs', newssrcs); // Have to clear presence map to get rid of removed streams this.connection.emuc.clearPresenceMedia(); if (newssrcs.length > 0) { for (var i = 1; i <= newssrcs.length; i ++) { // Change video type to screen if (newssrcs[i-1].type === 'video' && APP.desktopsharing.isUsingScreenStream()) { newssrcs[i-1].type = 'screen'; } this.connection.emuc.addMediaToPresence(i, newssrcs[i-1].type, newssrcs[i-1].ssrc, newssrcs[i-1].direction); } this.connection.emuc.sendPresence(); } } // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 function sendKeyframe(pc) { console.log('sendkeyframe', pc.iceConnectionState); if (pc.iceConnectionState !== 'connected') return; // safe... pc.setRemoteDescription( pc.remoteDescription, function () { pc.createAnswer( function (modifiedAnswer) { pc.setLocalDescription( modifiedAnswer, function () { // noop }, function (error) { console.log('triggerKeyframe setLocalDescription failed', error); APP.UI.messageHandler.showError(); } ); }, function (error) { console.log('triggerKeyframe createAnswer failed', error); APP.UI.messageHandler.showError(); } ); }, function (error) { console.log('triggerKeyframe setRemoteDescription failed', error); APP.UI.messageHandler.showError(); } ); } JingleSession.prototype.remoteStreamAdded = function (data, times) { var self = this; var thessrc; var ssrc2jid = this.connection.emuc.ssrc2jid; // look up an associated JID for a stream id if (data.stream.id && data.stream.id.indexOf('mixedmslabel') === -1) { // look only at a=ssrc: and _not_ at a=ssrc-group: lines var ssrclines = SDPUtil.find_lines(this.peerconnection.remoteDescription.sdp, 'a=ssrc:'); ssrclines = ssrclines.filter(function (line) { // NOTE(gp) previously we filtered on the mslabel, but that property // is not always present. // return line.indexOf('mslabel:' + data.stream.label) !== -1; return ((line.indexOf('msid:' + data.stream.id) !== -1)); }); if (ssrclines.length) { thessrc = ssrclines[0].substring(7).split(' ')[0]; // We signal our streams (through Jingle to the focus) before we set // our presence (through which peers associate remote streams to // jids). So, it might arrive that a remote stream is added but // ssrc2jid is not yet updated and thus data.peerjid cannot be // successfully set. Here we wait for up to a second for the // presence to arrive. if (!ssrc2jid[thessrc]) { if (typeof times === 'undefined') { times = 0; } if (times > 10) { console.warning('Waiting for jid timed out', thessrc); } else { setTimeout(function(d) { return function() { self.remoteStreamAdded(d, times++); } }(data), 250); } return; } // ok to overwrite the one from focus? might save work in colibri.js console.log('associated jid', ssrc2jid[thessrc], data.peerjid); if (ssrc2jid[thessrc]) { data.peerjid = ssrc2jid[thessrc]; } } } APP.RTC.createRemoteStream(data, this.sid, thessrc); var isVideo = data.stream.getVideoTracks().length > 0; // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 if (isVideo && data.peerjid && this.peerjid === data.peerjid && data.stream.getVideoTracks().length === 0 && APP.RTC.localVideo.getTracks().length > 0) { window.setTimeout(function () { sendKeyframe(self.peerconnection); }, 3000); } } module.exports = JingleSession; },{"../../service/RTC/RTCBrowserType":98,"./SDP":49,"./SDPDiffer":50,"./SDPUtil":51,"./TraceablePeerConnection":52,"async":62,"sdp-transform":94}],49:[function(require,module,exports){ /* jshint -W117 */ var SDPUtil = require("./SDPUtil"); // SDP STUFF function SDP(sdp) { 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 semantics = line.substr(0, idx).substr(13); var ssrcs = line.substr(14 + semantics.length).split(' '); if (ssrcs.length != 0) { media.ssrcGroups.push({ semantics: semantics, ssrcs: ssrcs }); } }); } return media_ssrcs; }; /** * Returns true if this SDP contains given SSRC. * @param ssrc the ssrc to check. * @returns {boolean} true if this SDP contains given SSRC. */ SDP.prototype.containsSSRC = function(ssrc) { var medias = this.getMediaSsrcMap(); var contains = false; Object.keys(medias).forEach(function(mediaindex){ var media = medias[mediaindex]; //console.log("Check", channel, ssrc); if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){ contains = true; } }); return contains; }; // 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, ssrcs) { // console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]); var i, j, k, mline, ssrc, rtpmap, tmp, line, lines; var self = this; // 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 { if(ssrcs && ssrcs[mline.media]) { ssrc = ssrcs[mline.media]; } 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 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) { 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 { elem.attrs({ name: kv.split(':', 2)[0] }); elem.attrs({ value: kv.split(':', 2)[1] }); } elem.up(); }); 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 = msid.replace(/[\{,\}]/g,""); 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) { idx = line.indexOf(' '); var semantics = line.substr(0, idx).substr(13); var ssrcs = line.substr(14 + semantics.length).split(' '); if (ssrcs.length != 0) { 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 i = mediaindex; var tmp; var self = this; elem.c('transport'); // XEP-0343 DTLS/SCTP if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length) { var sctpmap = SDPUtil.find_line( this.media[i], 'a=sctpmap:', self.session); if (sctpmap) { var 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 var 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) { elem.c('candidate', SDPUtil.candidateToJingle(line)).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'; // // 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 () { media += SDPUtil.candidateFromJingle(this); }); // XEP-0339 handle ssrc-group attributes tmp = 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 != 0) { 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 () { media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name'); if (this.getAttribute('value') && this.getAttribute('value').length) media += ':' + this.getAttribute('value'); media += '\r\n'; }); }); return media; }; module.exports = SDP; },{"./SDPUtil":51}],50:[function(require,module,exports){ function SDPDiffer(mySDP, otherSDP) { this.mySDP = mySDP; this.otherSDP = otherSDP; } /** * Returns map of MediaChannel that contains only media not contained in otherSdp. Mapped by channel idx. * @param otherSdp the other SDP to check ssrc with. */ 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 accross 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; }; /** * Sends SSRC update IQ. * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove. * @param sid session identifier that will be put into the IQ. * @param initiator initiator identifier. * @param toJid destination Jid * @param isAdd indicates if this is remove or add operation. */ SDPDiffer.prototype.toJingle = function(modify) { var sdpMediaSsrcs = this.getNewMedia(); var self = this; // FIXME: only announce video ssrcs since we mix audio and dont need // the audio ssrcs therefore 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 completly 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 { modify.attrs({ name: kv.split(':', 2)[0] }); modify.attrs({ value: kv.split(':', 2)[1] }); } modify.up(); // end of parameter }); modify.up(); // end of source }); // generate source groups from lines media.ssrcGroups.forEach(function(ssrcGroup) { if (ssrcGroup.ssrcs.length != 0) { 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; },{}],51:[function(require,module,exports){ SDPUtil = { 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 // 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 += ' '; line += cand.getAttribute('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 (cand.getAttribute('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; },{}],52:[function(require,module,exports){ function TraceablePeerConnection(ice_config, constraints) { var self = this; var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection; this.peerconnection = new RTCPeerconnection(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); self.updateLog.push({ time: new Date(), type: what, value: info || "" }); }; this.onicecandidate = null; this.peerconnection.onicecandidate = function (event) { 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); } }; if (!navigator.mozGetUserMedia && this.maxstats) { this.statsinterval = window.setInterval(function() { self.peerconnection.getStats(function(stats) { var results = stats.result(); for (var i = 0; i < results.length; ++i) { //console.log(results[i].type, results[i].id, results[i].names()) var now = new Date(); 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); } }; dumpSDP = function(description) { if (typeof description === 'undefined' || description == null) { return ''; } return 'type: ' + description.type + '\r\n' + description.sdp; }; 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 (navigator.mozGetUserMedia) { 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 (navigator.mozGetUserMedia) { 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); return; } }; TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) { this.trace('removeStream', stream.id); if(stopStreams) { stream.getAudioTracks().forEach(function (track) { track.stop(); }); stream.getVideoTracks().forEach(function (track) { track.stop(); }); } try { // FF doesn't support this yet. 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 (navigator.mozGetUserMedia) { 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); 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 (navigator.mozGetUserMedia) { description = this.interop.toUnifiedPlan(description); this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description)); } var self = this; this.peerconnection.setRemoteDescription(description, function () { self.trace('setRemoteDescriptionOnSuccess'); successCallback(); }, function (err) { self.trace('setRemoteDescriptionOnFailure', err); 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)); // if we're running on FF, transform to Plan B first. // NOTE this is not tested because in meet the focus generates the // offer. if (navigator.mozGetUserMedia) { offer = self.interop.toPlanB(offer); self.trace('createOfferOnSuccess::postTransform (Plan B)', 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); 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::preTransfom', dumpSDP(answer)); // if we're running on FF, transform to Plan A first. if (navigator.mozGetUserMedia) { answer = self.interop.toPlanB(answer); self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer)); } if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer); self.trace('createAnswerOnSuccess::postTransfom (simulcast)', dumpSDP(answer)); } successCallback(answer); }, function(err) { self.trace('createAnswerOnFailure', err); 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) { if (navigator.mozGetUserMedia) { // ignore for now... if(!errback) errback = function () { } this.peerconnection.getStats(null,callback,errback); } else { this.peerconnection.getStats(callback); } }; module.exports = TraceablePeerConnection; },{"sdp-interop":84,"sdp-simulcast":91}],53:[function(require,module,exports){ /* global $, $iq, APP, config, connection, UI, 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 = config.hosts.call_control !== undefined; 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; // 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(); } // 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) { APP.UI.messageHandler.notify( null, "notify.focus", 'disconnected', "notify.focusFail", {component: focusComponent, ms: 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; },{"../../service/authentication/AuthenticationEvents":103,"../../service/xmpp/XMPPEvents":108,"../settings/Settings":43}],54:[function(require,module,exports){ /* global $, $iq, config, connection, focusMucJid, messageHandler, Moderator, 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 = (typeof config.hosts.jirecon != "undefined"); /** * 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; function setRecordingToken(token) { recordingToken = token; } function setRecording(state, token, callback, connection) { if (useJirecon){ setRecordingJirecon(state, token, callback, connection); } else { setRecordingColibri(state, token, callback, connection); } } 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 ? 'start' : 'stop', mucjid: connection.emuc.roomjid}); if (!state){ 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 ? 'started' : 'stopped') + '(jirecon)' + result); recordingEnabled = state; if (!state){ 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 = ('true' === recordingElem.attr('state')); recordingEnabled = newState; callback(newState); }, function (error) { console.warn(error); callback(recordingEnabled); } ); } var Recording = { toggleRecording: function (tokenEmptyCallback, startingCallback, startedCallback, 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, startingCallback, startedCallback, connection); }); return; } var oldState = recordingEnabled; startingCallback(!oldState); setRecording(!oldState, 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); } startedCallback(state); }, connection ); } } module.exports = Recording; },{"./moderator":53}],55:[function(require,module,exports){ /* jshint -W117 */ /* a simple MUC connection plugin * can only handle a single MUC room */ var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var Moderator = require("./moderator"); var JingleSession = require("./JingleSession"); var bridgeIsDown = false; 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: {}, joined: false, isOwner: false, role: null, focusMucJid: null, ssrc2jid: {}, 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.isMuted()) { this.addAudioInfoToPresence(true); } if(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()); } } // Parse prezi tag. var presentation = $(pres).find('>prezi'); if (presentation.length) { var 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) { var url = this.preziMap[from]; delete this.preziMap[from]; $(document).trigger('presentationremoved.muc', [from, url]); } // Parse audio info tag. var audioMuted = $(pres).find('>audiomuted'); if (audioMuted.length) { $(document).trigger('audiomuted.muc', [from, audioMuted.text()]); } // Parse video info tag. var videoMuted = $(pres).find('>videomuted'); if (videoMuted.length) { $(document).trigger('videomuted.muc', [from, videoMuted.text()]); } var startMuted = $(pres).find('>startmuted'); if (startMuted.length) { eventEmitter.emit(XMPPEvents.START_MUTED, 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 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.html() : 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; 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); } } // 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; } var self = this; // Remove old ssrcs coming from the jid Object.keys(this.ssrc2jid).forEach(function (ssrc) { if (self.ssrc2jid[ssrc] == from) { delete self.ssrc2jid[ssrc]; } }); // 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); } } 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(); APP.UI.messageHandler.openReportDialog(null, "dialog.joinError", pres); } else { console.warn('onPresError ', pres); APP.UI.messageHandler.openReportDialog(null, "dialog.connectError", pres); } } else { console.warn('onPresError ', pres); APP.UI.messageHandler.openReportDialog(null, "dialog.connectError", 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); } } if (txt) { console.log('chat', nick, txt); eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED, from, nick, txt, this.myroomjid); } 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['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(); } 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; }, 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; }, addDevicesToPresence: function (devices) { this.presMap['devices'] = devices; }, clearPresenceMedia: function () { var self = this; Object.keys(this.presMap).forEach(function (key) { if (key.indexOf('source') != -1) { delete self.presMap[key]; } }); }, 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, memeber, pres) { if($(pres).find(">bridgeIsDown").length > 0 && !bridgeIsDown) { bridgeIsDown = true; eventEmitter.emit(XMPPEvents.BRIDGE_DOWN); } if(memeber.isFocus) return; var self = this; // Remove old ssrcs coming from the jid Object.keys(this.ssrc2jid).forEach(function (ssrc) { if (self.ssrc2jid[ssrc] == from) { delete self.ssrc2jid[ssrc]; } }); var changedStreams = []; $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) { //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc')); var ssrcV = ssrc.getAttribute('ssrc'); self.ssrc2jid[ssrcV] = from; var type = ssrc.getAttribute('type'); var direction = ssrc.getAttribute('direction'); changedStreams.push({type: type, direction: direction}); }); eventEmitter.emit(XMPPEvents.STREAMS_CHANGED, from, changedStreams); var displayName = !config.displayJids ? memeber.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); } }); }; },{"../../service/xmpp/XMPPEvents":108,"./JingleSession":48,"./moderator":53}],56:[function(require,module,exports){ /* jshint -W117 */ var JingleSession = require("./JingleSession"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); module.exports = function(XMPP, eventEmitter) { function CallIncomingJingle(sid, connection) { var sess = connection.jingle.sessions[sid]; // TODO: do we check activecall == null? connection.jingle.activecall = sess; eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess); // TODO: check affiliation and/or role console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); sess.usedrip = true; // not-so-naive trickle ice sess.sendAnswer(); sess.accept(); }; 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'); // this is dealt with by SDP O/A so we don't need to annouce 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 if (config.useRtcpMux) { this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux } if (config.useBundle) { 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; } // compare from to sess.peerjid (bare jid comparison for later compat with message-mode) // local jid is not checked if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(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': var startMuted = $(iq).find('jingle>startmuted'); if(startMuted && startMuted.length > 0) { var audioMuted = startMuted.attr("audio"); var videoMuted = startMuted.attr("video"); APP.UI.setInitialMuteFromFocus((audioMuted === "true"), (videoMuted === "true")); } sess = new JingleSession( $(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection, XMPP); // configure session sess.media_constraints = this.media_constraints; sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; sess.initiate(fromJid, false); // FIXME: setRemoteDescription should only be done when this call is to be accepted sess.setRemoteDescription($(iq).find('>jingle'), 'offer'); this.sessions[sess.sid] = sess; this.jid2session[sess.peerjid] = sess; // the callback should either // .sendAnswer and .accept // or .sendTerminate -- not necessarily synchronus CallIncomingJingle(sess.sid, this.connection); break; case 'session-accept': sess.setRemoteDescription($(iq).find('>jingle'), 'answer'); sess.accept(); $(document).trigger('callaccepted.jingle', [sess.sid]); break; case 'session-terminate': // If this is not the focus sending the terminate, we have // nothing more to do here. if (Object.keys(this.sessions).length < 1 || !(this.sessions[Object.keys(this.sessions)[0]] instanceof JingleSession)) { 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 sess.addSource($(iq).find('>jingle>content'), fromJid); break; case 'removesource': // FIXME: proprietary, un-jingleish case 'source-remove': // FIXME: proprietary sess.removeSource($(iq).find('>jingle>content'), fromJid); 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); // configure session sess.media_constraints = this.media_constraints; sess.pc_constraints = this.pc_constraints; sess.ice_config = this.ice_config; sess.initiate(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? }, /** * Populates the log data */ populateData: 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; } }); }; },{"../../service/xmpp/XMPPEvents":108,"./JingleSession":48}],57:[function(require,module,exports){ /* 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]); } }); }; },{}],58:[function(require,module,exports){ /* global $, $iq, config, connection, focusMucJid, forceMuted, setAudioMuted, Strophe */ /** * Moderate connection plugin. */ module.exports = function (XMPP) { 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"; APP.UI.setAudioMuted(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); } }); } },{}],59:[function(require,module,exports){ /* 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'); this.call_resource = resource.substr('xmpp:'.length); console.info( "Received call resource: " + this.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; } ); } } ); }; },{}],60:[function(require,module,exports){ /** * 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"; } }; }; },{}],61:[function(require,module,exports){ /* global $, APP, config, Strophe*/ var Moderator = require("./moderator"); var EventEmitter = require("events"); var Recording = require("./recording"); var SDP = require("./SDP"); var Settings = require("../settings/Settings"); var Pako = require("pako"); var StreamEventTypes = require("../../service/RTC/StreamEventTypes"); var RTCEvents = require("../../service/RTC/RTCEvents"); var UIEvents = require("../../service/UI/UIEvents"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var retry = require('retry'); var eventEmitter = new EventEmitter(); var connection = null; var authenticatedUser = false; function connect(jid, password) { var faultTolerantConnect = retry.operation({ retries: 3 }); // fault tolerant connect faultTolerantConnect.attempt(function () { connection = XMPP.createConnection(); Moderator.setConnection(connection); if (connection.disco) { // for chrome, add multistream cap } 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('Strophe status changed to', Strophe.getStatusString(status), msg); if (status === Strophe.Status.CONNECTED) { if (config.useStunTurn) { connection.jingle.getStunAndTurnCredentials(); } console.info("My Jabber ID: " + connection.jid); 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) { 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() { var roomName = APP.UI.generateRoomName(); Moderator.allocateConferenceFocus( roomName, APP.UI.checkForNicknameAndJoin); } function initStrophePlugins() { require("./strophe.emuc")(XMPP, eventEmitter); require("./strophe.jingle")(XMPP, eventEmitter); require("./strophe.moderate")(XMPP); require("./strophe.util")(); require("./strophe.rayo")(); require("./strophe.logger")(); } function registerListeners() { APP.RTC.addStreamListener(maybeDoJoin, StreamEventTypes.EVENT_TYPE_LOCAL_CREATED); APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) { XMPP.addToPresence("devices", devices); }) APP.UI.addListener(UIEvents.NICKNAME_CHANGED, function (nickname) { XMPP.addToPresence("displayName", nickname); }); } 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 garante 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); 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, null); }, createConnection: function () { var bosh = config.bosh || '/http-bind'; return new Strophe.Connection(bosh); }, getStatusString: function (status) { return Strophe.getStatusString(status); }, promptLogin: function () { // FIXME: re-use LoginDialog which supports retries APP.UI.showLoginPopup(connect); }, joinRoom: function(roomName, useNicks, nick) { var roomjid; 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); }, disposeConference: function (onUnload) { eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, 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(); } 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(); }, switchStreams: function (stream, oldStream, callback, isAudio) { if (connection && connection.jingle.activecall) { // 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; } // It is not clear what is the right way to handle multiple tracks. // So at least make sure that they are all muted or all unmuted and // that we send presence just once. APP.RTC.localAudio.setMute(!mute); // isMuted is the opposite of audioEnabled this.sendAudioInfoPresence(mute, callback); return true; }, sendAudioInfoPresence: function(mute, callback) { if(connection) { connection.emuc.addAudioInfoToPresence(mute); connection.emuc.sendPresence(); } callback(); return true; }, // Really mute video, i.e. dont even send black frames muteVideo: function (pc, unmute) { // FIXME: this probably needs another of those lovely state safeguards... // which checks for iceconn == connected and sigstate == stable pc.setRemoteDescription(pc.remoteDescription, function () { pc.createAnswer( function (answer) { var sdp = new SDP(answer.sdp); if (sdp.media.length > 1) { if (unmute) sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); else sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly'); sdp.raw = sdp.session + sdp.media.join(''); answer.sdp = sdp.raw; } pc.setLocalDescription(answer, function () { console.log('mute SLD ok'); }, function (error) { console.log('mute SLD error'); APP.UI.messageHandler.showError("dialog.error", "dialog.SLDFailure"); } ); }, function (error) { console.log(error); APP.UI.messageHandler.showError(); } ); }, function (error) { console.log('muteVideo SRD error'); APP.UI.messageHandler.showError("dialog.error", "dialog.SRDFailure"); } ); }, toggleRecording: function (tokenEmptyCallback, startingCallback, startedCallback) { Recording.toggleRecording(tokenEmptyCallback, startingCallback, startedCallback, 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 "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; }, populateData: function () { var data = {}; if (connection.jingle) { data = connection.jingle.populateData(); } return data; }, getLogger: function () { if(connection.logger) return connection.logger.log; return 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(!connection) return null; return connection.emuc.ssrc2jid[ssrc]; }, getMUCJoined: function () { return connection.emuc.joined; }, getSessions: function () { return connection.jingle.sessions; }, removeStream: function (stream) { if(!connection || !connection.jingle.activecall || !connection.jingle.activecall.peerconnection) return; connection.jingle.activecall.peerconnection.removeStream(stream); } }; module.exports = XMPP; },{"../../service/RTC/RTCEvents":99,"../../service/RTC/StreamEventTypes":101,"../../service/UI/UIEvents":102,"../../service/xmpp/XMPPEvents":108,"../settings/Settings":43,"./SDP":49,"./moderator":53,"./recording":54,"./strophe.emuc":55,"./strophe.jingle":56,"./strophe.logger":57,"./strophe.moderate":58,"./strophe.rayo":59,"./strophe.util":60,"events":1,"pako":64,"retry":80}],62:[function(require,module,exports){ (function (process){ /*! * async * https://github.com/caolan/async * * Copyright 2010-2014 Caolan McMahon * Released under the MIT license */ /*jshint onevar: false, indent:4 */ /*global setImmediate: false, setTimeout: false, console: false */ (function () { var async = {}; // global on the server, window in the browser var root, previous_async; root = this; if (root != null) { previous_async = root.async; } async.noConflict = function () { root.async = previous_async; return async; }; function only_once(fn) { var called = false; return function() { if (called) throw new Error("Callback was already called."); called = true; fn.apply(root, arguments); } } //// cross-browser compatiblity functions //// var _toString = Object.prototype.toString; var _isArray = Array.isArray || function (obj) { return _toString.call(obj) === '[object Array]'; }; var _each = function (arr, iterator) { if (arr.forEach) { return arr.forEach(iterator); } for (var i = 0; i < arr.length; i += 1) { iterator(arr[i], i, arr); } }; var _map = function (arr, iterator) { if (arr.map) { return arr.map(iterator); } var results = []; _each(arr, function (x, i, a) { results.push(iterator(x, i, a)); }); return results; }; var _reduce = function (arr, iterator, memo) { if (arr.reduce) { return arr.reduce(iterator, memo); } _each(arr, function (x, i, a) { memo = iterator(memo, x, i, a); }); return memo; }; var _keys = function (obj) { if (Object.keys) { return Object.keys(obj); } var keys = []; for (var k in obj) { if (obj.hasOwnProperty(k)) { keys.push(k); } } return keys; }; //// exported async module functions //// //// nextTick implementation with browser-compatible fallback //// if (typeof process === 'undefined' || !(process.nextTick)) { if (typeof setImmediate === 'function') { async.nextTick = function (fn) { // not a direct alias for IE10 compatibility setImmediate(fn); }; async.setImmediate = async.nextTick; } else { async.nextTick = function (fn) { setTimeout(fn, 0); }; async.setImmediate = async.nextTick; } } else { async.nextTick = process.nextTick; if (typeof setImmediate !== 'undefined') { async.setImmediate = function (fn) { // not a direct alias for IE10 compatibility setImmediate(fn); }; } else { async.setImmediate = async.nextTick; } } async.each = function (arr, iterator, callback) { callback = callback || function () {}; if (!arr.length) { return callback(); } var completed = 0; _each(arr, function (x) { iterator(x, only_once(done) ); }); function done(err) { if (err) { callback(err); callback = function () {}; } else { completed += 1; if (completed >= arr.length) { callback(); } } } }; async.forEach = async.each; async.eachSeries = function (arr, iterator, callback) { callback = callback || function () {}; if (!arr.length) { return callback(); } var completed = 0; var iterate = function () { iterator(arr[completed], function (err) { if (err) { callback(err); callback = function () {}; } else { completed += 1; if (completed >= arr.length) { callback(); } else { iterate(); } } }); }; iterate(); }; async.forEachSeries = async.eachSeries; async.eachLimit = function (arr, limit, iterator, callback) { var fn = _eachLimit(limit); fn.apply(null, [arr, iterator, callback]); }; async.forEachLimit = async.eachLimit; var _eachLimit = function (limit) { return function (arr, iterator, callback) { callback = callback || function () {}; if (!arr.length || limit <= 0) { return callback(); } var completed = 0; var started = 0; var running = 0; (function replenish () { if (completed >= arr.length) { return callback(); } while (running < limit && started < arr.length) { started += 1; running += 1; iterator(arr[started - 1], function (err) { if (err) { callback(err); callback = function () {}; } else { completed += 1; running -= 1; if (completed >= arr.length) { callback(); } else { replenish(); } } }); } })(); }; }; var doParallel = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); return fn.apply(null, [async.each].concat(args)); }; }; var doParallelLimit = function(limit, fn) { return function () { var args = Array.prototype.slice.call(arguments); return fn.apply(null, [_eachLimit(limit)].concat(args)); }; }; var doSeries = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); return fn.apply(null, [async.eachSeries].concat(args)); }; }; var _asyncMap = function (eachfn, arr, iterator, callback) { arr = _map(arr, function (x, i) { return {index: i, value: x}; }); if (!callback) { eachfn(arr, function (x, callback) { iterator(x.value, function (err) { callback(err); }); }); } else { var results = []; eachfn(arr, function (x, callback) { iterator(x.value, function (err, v) { results[x.index] = v; callback(err); }); }, function (err) { callback(err, results); }); } }; async.map = doParallel(_asyncMap); async.mapSeries = doSeries(_asyncMap); async.mapLimit = function (arr, limit, iterator, callback) { return _mapLimit(limit)(arr, iterator, callback); }; var _mapLimit = function(limit) { return doParallelLimit(limit, _asyncMap); }; // reduce only has a series version, as doing reduce in parallel won't // work in many situations. async.reduce = function (arr, memo, iterator, callback) { async.eachSeries(arr, function (x, callback) { iterator(memo, x, function (err, v) { memo = v; callback(err); }); }, function (err) { callback(err, memo); }); }; // inject alias async.inject = async.reduce; // foldl alias async.foldl = async.reduce; async.reduceRight = function (arr, memo, iterator, callback) { var reversed = _map(arr, function (x) { return x; }).reverse(); async.reduce(reversed, memo, iterator, callback); }; // foldr alias async.foldr = async.reduceRight; var _filter = function (eachfn, arr, iterator, callback) { var results = []; arr = _map(arr, function (x, i) { return {index: i, value: x}; }); eachfn(arr, function (x, callback) { iterator(x.value, function (v) { if (v) { results.push(x); } callback(); }); }, function (err) { callback(_map(results.sort(function (a, b) { return a.index - b.index; }), function (x) { return x.value; })); }); }; async.filter = doParallel(_filter); async.filterSeries = doSeries(_filter); // select alias async.select = async.filter; async.selectSeries = async.filterSeries; var _reject = function (eachfn, arr, iterator, callback) { var results = []; arr = _map(arr, function (x, i) { return {index: i, value: x}; }); eachfn(arr, function (x, callback) { iterator(x.value, function (v) { if (!v) { results.push(x); } callback(); }); }, function (err) { callback(_map(results.sort(function (a, b) { return a.index - b.index; }), function (x) { return x.value; })); }); }; async.reject = doParallel(_reject); async.rejectSeries = doSeries(_reject); var _detect = function (eachfn, arr, iterator, main_callback) { eachfn(arr, function (x, callback) { iterator(x, function (result) { if (result) { main_callback(x); main_callback = function () {}; } else { callback(); } }); }, function (err) { main_callback(); }); }; async.detect = doParallel(_detect); async.detectSeries = doSeries(_detect); async.some = function (arr, iterator, main_callback) { async.each(arr, function (x, callback) { iterator(x, function (v) { if (v) { main_callback(true); main_callback = function () {}; } callback(); }); }, function (err) { main_callback(false); }); }; // any alias async.any = async.some; async.every = function (arr, iterator, main_callback) { async.each(arr, function (x, callback) { iterator(x, function (v) { if (!v) { main_callback(false); main_callback = function () {}; } callback(); }); }, function (err) { main_callback(true); }); }; // all alias async.all = async.every; async.sortBy = function (arr, iterator, callback) { async.map(arr, function (x, callback) { iterator(x, function (err, criteria) { if (err) { callback(err); } else { callback(null, {value: x, criteria: criteria}); } }); }, function (err, results) { if (err) { return callback(err); } else { var fn = function (left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }; callback(null, _map(results.sort(fn), function (x) { return x.value; })); } }); }; async.auto = function (tasks, callback) { callback = callback || function () {}; var keys = _keys(tasks); var remainingTasks = keys.length if (!remainingTasks) { return callback(); } var results = {}; var listeners = []; var addListener = function (fn) { listeners.unshift(fn); }; var removeListener = function (fn) { for (var i = 0; i < listeners.length; i += 1) { if (listeners[i] === fn) { listeners.splice(i, 1); return; } } }; var taskComplete = function () { remainingTasks-- _each(listeners.slice(0), function (fn) { fn(); }); }; addListener(function () { if (!remainingTasks) { var theCallback = callback; // prevent final callback from calling itself if it errors callback = function () {}; theCallback(null, results); } }); _each(keys, function (k) { var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; var taskCallback = function (err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; } if (err) { var safeResults = {}; _each(_keys(results), function(rkey) { safeResults[rkey] = results[rkey]; }); safeResults[k] = args; callback(err, safeResults); // stop subsequent errors hitting callback multiple times callback = function () {}; } else { results[k] = args; async.setImmediate(taskComplete); } }; var requires = task.slice(0, Math.abs(task.length - 1)) || []; var ready = function () { return _reduce(requires, function (a, x) { return (a && results.hasOwnProperty(x)); }, true) && !results.hasOwnProperty(k); }; if (ready()) { task[task.length - 1](taskCallback, results); } else { var listener = function () { if (ready()) { removeListener(listener); task[task.length - 1](taskCallback, results); } }; addListener(listener); } }); }; async.retry = function(times, task, callback) { var DEFAULT_TIMES = 5; var attempts = []; // Use defaults if times not passed if (typeof times === 'function') { callback = task; task = times; times = DEFAULT_TIMES; } // Make sure times is a number times = parseInt(times, 10) || DEFAULT_TIMES; var wrappedTask = function(wrappedCallback, wrappedResults) { var retryAttempt = function(task, finalAttempt) { return function(seriesCallback) { task(function(err, result){ seriesCallback(!err || finalAttempt, {err: err, result: result}); }, wrappedResults); }; }; while (times) { attempts.push(retryAttempt(task, !(times-=1))); } async.series(attempts, function(done, data){ data = data[data.length - 1]; (wrappedCallback || callback)(data.err, data.result); }); } // If a callback is passed, run this as a controll flow return callback ? wrappedTask() : wrappedTask }; async.waterfall = function (tasks, callback) { callback = callback || function () {}; if (!_isArray(tasks)) { var err = new Error('First argument to waterfall must be an array of functions'); return callback(err); } if (!tasks.length) { return callback(); } var wrapIterator = function (iterator) { return function (err) { if (err) { callback.apply(null, arguments); callback = function () {}; } else { var args = Array.prototype.slice.call(arguments, 1); var next = iterator.next(); if (next) { args.push(wrapIterator(next)); } else { args.push(callback); } async.setImmediate(function () { iterator.apply(null, args); }); } }; }; wrapIterator(async.iterator(tasks))(); }; var _parallel = function(eachfn, tasks, callback) { callback = callback || function () {}; if (_isArray(tasks)) { eachfn.map(tasks, function (fn, callback) { if (fn) { fn(function (err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; } callback.call(null, err, args); }); } }, callback); } else { var results = {}; eachfn.each(_keys(tasks), function (k, callback) { tasks[k](function (err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; } results[k] = args; callback(err); }); }, function (err) { callback(err, results); }); } }; async.parallel = function (tasks, callback) { _parallel({ map: async.map, each: async.each }, tasks, callback); }; async.parallelLimit = function(tasks, limit, callback) { _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); }; async.series = function (tasks, callback) { callback = callback || function () {}; if (_isArray(tasks)) { async.mapSeries(tasks, function (fn, callback) { if (fn) { fn(function (err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; } callback.call(null, err, args); }); } }, callback); } else { var results = {}; async.eachSeries(_keys(tasks), function (k, callback) { tasks[k](function (err) { var args = Array.prototype.slice.call(arguments, 1); if (args.length <= 1) { args = args[0]; } results[k] = args; callback(err); }); }, function (err) { callback(err, results); }); } }; async.iterator = function (tasks) { var makeCallback = function (index) { var fn = function () { if (tasks.length) { tasks[index].apply(null, arguments); } return fn.next(); }; fn.next = function () { return (index < tasks.length - 1) ? makeCallback(index + 1): null; }; return fn; }; return makeCallback(0); }; async.apply = function (fn) { var args = Array.prototype.slice.call(arguments, 1); return function () { return fn.apply( null, args.concat(Array.prototype.slice.call(arguments)) ); }; }; var _concat = function (eachfn, arr, fn, callback) { var r = []; eachfn(arr, function (x, cb) { fn(x, function (err, y) { r = r.concat(y || []); cb(err); }); }, function (err) { callback(err, r); }); }; async.concat = doParallel(_concat); async.concatSeries = doSeries(_concat); async.whilst = function (test, iterator, callback) { if (test()) { iterator(function (err) { if (err) { return callback(err); } async.whilst(test, iterator, callback); }); } else { callback(); } }; async.doWhilst = function (iterator, test, callback) { iterator(function (err) { if (err) { return callback(err); } var args = Array.prototype.slice.call(arguments, 1); if (test.apply(null, args)) { async.doWhilst(iterator, test, callback); } else { callback(); } }); }; async.until = function (test, iterator, callback) { if (!test()) { iterator(function (err) { if (err) { return callback(err); } async.until(test, iterator, callback); }); } else { callback(); } }; async.doUntil = function (iterator, test, callback) { iterator(function (err) { if (err) { return callback(err); } var args = Array.prototype.slice.call(arguments, 1); if (!test.apply(null, args)) { async.doUntil(iterator, test, callback); } else { callback(); } }); }; async.queue = function (worker, concurrency) { if (concurrency === undefined) { concurrency = 1; } function _insert(q, data, pos, callback) { if (!q.started){ q.started = true; } if (!_isArray(data)) { data = [data]; } if(data.length == 0) { // call drain immediately if there are no tasks return async.setImmediate(function() { if (q.drain) { q.drain(); } }); } _each(data, function(task) { var item = { data: task, callback: typeof callback === 'function' ? callback : null }; if (pos) { q.tasks.unshift(item); } else { q.tasks.push(item); } if (q.saturated && q.tasks.length === q.concurrency) { q.saturated(); } async.setImmediate(q.process); }); } var workers = 0; var q = { tasks: [], concurrency: concurrency, saturated: null, empty: null, drain: null, started: false, paused: false, push: function (data, callback) { _insert(q, data, false, callback); }, kill: function () { q.drain = null; q.tasks = []; }, unshift: function (data, callback) { _insert(q, data, true, callback); }, process: function () { if (!q.paused && workers < q.concurrency && q.tasks.length) { var task = q.tasks.shift(); if (q.empty && q.tasks.length === 0) { q.empty(); } workers += 1; var next = function () { workers -= 1; if (task.callback) { task.callback.apply(task, arguments); } if (q.drain && q.tasks.length + workers === 0) { q.drain(); } q.process(); }; var cb = only_once(next); worker(task.data, cb); } }, length: function () { return q.tasks.length; }, running: function () { return workers; }, idle: function() { return q.tasks.length + workers === 0; }, pause: function () { if (q.paused === true) { return; } q.paused = true; q.process(); }, resume: function () { if (q.paused === false) { return; } q.paused = false; q.process(); } }; return q; }; async.priorityQueue = function (worker, concurrency) { function _compareTasks(a, b){ return a.priority - b.priority; }; function _binarySearch(sequence, item, compare) { var beg = -1, end = sequence.length - 1; while (beg < end) { var mid = beg + ((end - beg + 1) >>> 1); if (compare(item, sequence[mid]) >= 0) { beg = mid; } else { end = mid - 1; } } return beg; } function _insert(q, data, priority, callback) { if (!q.started){ q.started = true; } if (!_isArray(data)) { data = [data]; } if(data.length == 0) { // call drain immediately if there are no tasks return async.setImmediate(function() { if (q.drain) { q.drain(); } }); } _each(data, function(task) { var item = { data: task, priority: priority, callback: typeof callback === 'function' ? callback : null }; q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); if (q.saturated && q.tasks.length === q.concurrency) { q.saturated(); } async.setImmediate(q.process); }); } // Start with a normal queue var q = async.queue(worker, concurrency); // Override push to accept second parameter representing priority q.push = function (data, priority, callback) { _insert(q, data, priority, callback); }; // Remove unshift function delete q.unshift; return q; }; async.cargo = function (worker, payload) { var working = false, tasks = []; var cargo = { tasks: tasks, payload: payload, saturated: null, empty: null, drain: null, drained: true, push: function (data, callback) { if (!_isArray(data)) { data = [data]; } _each(data, function(task) { tasks.push({ data: task, callback: typeof callback === 'function' ? callback : null }); cargo.drained = false; if (cargo.saturated && tasks.length === payload) { cargo.saturated(); } }); async.setImmediate(cargo.process); }, process: function process() { if (working) return; if (tasks.length === 0) { if(cargo.drain && !cargo.drained) cargo.drain(); cargo.drained = true; return; } var ts = typeof payload === 'number' ? tasks.splice(0, payload) : tasks.splice(0, tasks.length); var ds = _map(ts, function (task) { return task.data; }); if(cargo.empty) cargo.empty(); working = true; worker(ds, function () { working = false; var args = arguments; _each(ts, function (data) { if (data.callback) { data.callback.apply(null, args); } }); process(); }); }, length: function () { return tasks.length; }, running: function () { return working; } }; return cargo; }; var _console_fn = function (name) { return function (fn) { var args = Array.prototype.slice.call(arguments, 1); fn.apply(null, args.concat([function (err) { var args = Array.prototype.slice.call(arguments, 1); if (typeof console !== 'undefined') { if (err) { if (console.error) { console.error(err); } } else if (console[name]) { _each(args, function (x) { console[name](x); }); } } }])); }; }; async.log = _console_fn('log'); async.dir = _console_fn('dir'); /*async.info = _console_fn('info'); async.warn = _console_fn('warn'); async.error = _console_fn('error');*/ async.memoize = function (fn, hasher) { var memo = {}; var queues = {}; hasher = hasher || function (x) { return x; }; var memoized = function () { var args = Array.prototype.slice.call(arguments); var callback = args.pop(); var key = hasher.apply(null, args); if (key in memo) { async.nextTick(function () { callback.apply(null, memo[key]); }); } else if (key in queues) { queues[key].push(callback); } else { queues[key] = [callback]; fn.apply(null, args.concat([function () { memo[key] = arguments; var q = queues[key]; delete queues[key]; for (var i = 0, l = q.length; i < l; i++) { q[i].apply(null, arguments); } }])); } }; memoized.memo = memo; memoized.unmemoized = fn; return memoized; }; async.unmemoize = function (fn) { return function () { return (fn.unmemoized || fn).apply(null, arguments); }; }; async.times = function (count, iterator, callback) { var counter = []; for (var i = 0; i < count; i++) { counter.push(i); } return async.map(counter, iterator, callback); }; async.timesSeries = function (count, iterator, callback) { var counter = []; for (var i = 0; i < count; i++) { counter.push(i); } return async.mapSeries(counter, iterator, callback); }; async.seq = function (/* functions... */) { var fns = arguments; return function () { var that = this; var args = Array.prototype.slice.call(arguments); var callback = args.pop(); async.reduce(fns, args, function (newargs, fn, cb) { fn.apply(that, newargs.concat([function () { var err = arguments[0]; var nextargs = Array.prototype.slice.call(arguments, 1); cb(err, nextargs); }])) }, function (err, results) { callback.apply(that, [err].concat(results)); }); }; }; async.compose = function (/* functions... */) { return async.seq.apply(null, Array.prototype.reverse.call(arguments)); }; var _applyEach = function (eachfn, fns /*args...*/) { var go = function () { var that = this; var args = Array.prototype.slice.call(arguments); var callback = args.pop(); return eachfn(fns, function (fn, cb) { fn.apply(that, args.concat([cb])); }, callback); }; if (arguments.length > 2) { var args = Array.prototype.slice.call(arguments, 2); return go.apply(this, args); } else { return go; } }; async.applyEach = doParallel(_applyEach); async.applyEachSeries = doSeries(_applyEach); async.forever = function (fn, callback) { function next(err) { if (err) { if (callback) { return callback(err); } throw err; } fn(next); } next(); }; // Node.js if (typeof module !== 'undefined' && module.exports) { module.exports = async; } // AMD / RequireJS else if (typeof define !== 'undefined' && define.amd) { define([], function () { return async; }); } // included directly via