diff --git a/index.html b/index.html index 6f7f2d76a..1483078f9 100644 --- a/index.html +++ b/index.html @@ -242,17 +242,5 @@ - diff --git a/libs/lib-jitsi-meet.js b/libs/lib-jitsi-meet.js index 4f6bc1a81..3d5459695 100644 --- a/libs/lib-jitsi-meet.js +++ b/libs/lib-jitsi-meet.js @@ -1001,7 +1001,7 @@ function setupListeners(conference) { module.exports = JitsiConference; }).call(this,"/JitsiConference.js") -},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiParticipant":8,"./JitsiTrackEvents":10,"./modules/DTMF/JitsiDTMFManager":11,"./modules/RTC/RTC":16,"./modules/settings/Settings":21,"./modules/statistics/statistics":25,"./service/RTC/RTCEvents":132,"./service/authentication/AuthenticationEvents":134,"./service/xmpp/XMPPEvents":138,"events":45,"jitsi-meet-logger":49}],2:[function(require,module,exports){ +},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiParticipant":8,"./JitsiTrackEvents":10,"./modules/DTMF/JitsiDTMFManager":11,"./modules/RTC/RTC":16,"./modules/settings/Settings":21,"./modules/statistics/statistics":25,"./service/RTC/RTCEvents":131,"./service/authentication/AuthenticationEvents":133,"./service/xmpp/XMPPEvents":137,"events":51,"jitsi-meet-logger":79}],2:[function(require,module,exports){ /** * Enumeration with the errors for the conference. * @type {{string: string}} @@ -1230,7 +1230,7 @@ JitsiConnection.prototype.removeEventListener = function (event, listener) { module.exports = JitsiConnection; -},{"./JitsiConference":1,"./modules/xmpp/xmpp":43}],5:[function(require,module,exports){ +},{"./JitsiConference":1,"./modules/xmpp/xmpp":44}],5:[function(require,module,exports){ /** * Enumeration with the errors for the connection. * @type {{string: string}} @@ -1292,6 +1292,7 @@ var Logger = require("jitsi-meet-logger"); var RTC = require("./modules/RTC/RTC"); var Statistics = require("./modules/statistics/statistics"); var Resolutions = require("./service/RTC/Resolutions"); +var ScriptUtil = require("./modules/util/ScriptUtil"); function getLowerResolution(resolution) { if(!Resolutions[resolution]) @@ -1411,7 +1412,15 @@ var LibJitsiMeet = { }, enumerateDevices: function (callback) { RTC.enumerateDevices(callback); - } + }, + + /** + * Represents a hub/namespace for utility functionality which may be of + * interest to LibJitsiMeet clients. + */ + util: { + ScriptUtil: ScriptUtil, + }, }; //Setups the promise object. @@ -1419,7 +1428,7 @@ window.Promise = window.Promise || require("es6-promise").Promise; module.exports = LibJitsiMeet; -},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiConnection":4,"./JitsiConnectionErrors":5,"./JitsiConnectionEvents":6,"./JitsiTrackErrors":9,"./JitsiTrackEvents":10,"./modules/RTC/RTC":16,"./modules/statistics/statistics":25,"./service/RTC/Resolutions":133,"es6-promise":47,"jitsi-meet-logger":49}],8:[function(require,module,exports){ +},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiConnection":4,"./JitsiConnectionErrors":5,"./JitsiConnectionEvents":6,"./JitsiTrackErrors":9,"./JitsiTrackEvents":10,"./modules/RTC/RTC":16,"./modules/statistics/statistics":25,"./modules/util/ScriptUtil":27,"./service/RTC/Resolutions":132,"es6-promise":72,"jitsi-meet-logger":79}],8:[function(require,module,exports){ /* global Strophe */ /** @@ -1643,7 +1652,7 @@ JitsiDTMFManager.prototype.sendTones = function (tones, duration, pause) { }; }).call(this,"/modules/DTMF/JitsiDTMFManager.js") -},{"jitsi-meet-logger":49}],12:[function(require,module,exports){ +},{"jitsi-meet-logger":79}],12:[function(require,module,exports){ (function (__filename){ /* global config, APP, Strophe */ @@ -1867,7 +1876,7 @@ module.exports = DataChannels; }).call(this,"/modules/RTC/DataChannels.js") -},{"../../service/RTC/RTCEvents":132,"jitsi-meet-logger":49}],13:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":131,"jitsi-meet-logger":79}],13:[function(require,module,exports){ var JitsiTrack = require("./JitsiTrack"); var RTCBrowserType = require("./RTCBrowserType"); var JitsiTrackEvents = require('../../JitsiTrackEvents'); @@ -1878,11 +1887,12 @@ var RTCUtils = require("./RTCUtils"); * @constructor */ function JitsiLocalTrack(stream, videoType, - resolution) + resolution, deviceId) { this.videoType = videoType; this.dontFireRemoveEvent = false; this.resolution = resolution; + this.deviceId = deviceId; this.startMuted = false; var self = this; JitsiTrack.call(this, null, stream, @@ -1941,9 +1951,16 @@ JitsiLocalTrack.prototype._setMute = function (mute) { //FIXME: Maybe here we should set the SRC for the containers to something } else { var self = this; - RTCUtils.obtainAudioAndVideoPermissions({ + var streamOptions = { devices: (isAudio ? ["audio"] : ["video"]), - resolution: self.resolution}) + resolution: self.resolution + }; + if (isAudio) { + streamOptions['micDeviceId'] = self.deviceId; + } else {self.videoType === 'camera'} { + streamOptions['cameraDeviceId'] = self.deviceId; + } + RTCUtils.obtainAudioAndVideoPermissions(streamOptions) .then(function (streams) { var stream = null; for(var i = 0; i < streams.length; i++) { @@ -2384,7 +2401,7 @@ JitsiTrack.prototype.setAudioLevel = function (audioLevel) { module.exports = JitsiTrack; -},{"../../JitsiTrackEvents":10,"./RTCBrowserType":17,"./RTCUtils":18,"events":45}],16:[function(require,module,exports){ +},{"../../JitsiTrackEvents":10,"./RTCBrowserType":17,"./RTCUtils":18,"events":51}],16:[function(require,module,exports){ /* global APP */ var EventEmitter = require("events"); var RTCBrowserType = require("./RTCBrowserType"); @@ -2398,11 +2415,17 @@ var DesktopSharingEventTypes var MediaStreamType = require("../../service/RTC/MediaStreamTypes"); var RTCEvents = require("../../service/RTC/RTCEvents.js"); -function createLocalTracks(streams) { +function createLocalTracks(streams, options) { var newStreams = [] + var deviceId = null; for (var i = 0; i < streams.length; i++) { + if (streams[i].type === 'audio') { + deviceId = options.micDeviceId; + } else if (streams[i].videoType === 'camera'){ + deviceId = options.cameraDeviceId; + } var localStream = new JitsiLocalTrack(streams[i].stream, - streams[i].videoType, streams[i].resolution); + streams[i].videoType, streams[i].resolution, deviceId); newStreams.push(localStream); if (streams[i].isMuted === true) localStream.setMute(true); @@ -2460,8 +2483,11 @@ function RTC(room, options) { * @param {string} options.micDeviceId * @returns {*} Promise object that will receive the new JitsiTracks */ + RTC.obtainAudioAndVideoPermissions = function (options) { - return RTCUtils.obtainAudioAndVideoPermissions(options).then(createLocalTracks); + return RTCUtils.obtainAudioAndVideoPermissions(options).then(function (streams) { + return createLocalTracks(streams, options); + }); } RTC.prototype.onIncommingCall = function(event) { @@ -2644,7 +2670,7 @@ RTC.prototype.setAudioLevel = function (jid, audioLevel) { } module.exports = RTC; -},{"../../service/RTC/MediaStreamTypes":131,"../../service/RTC/RTCEvents.js":132,"../../service/desktopsharing/DesktopSharingEventTypes":135,"./DataChannels":12,"./JitsiLocalTrack.js":13,"./JitsiRemoteTrack.js":14,"./JitsiTrack":15,"./RTCBrowserType":17,"./RTCUtils.js":18,"events":45}],17:[function(require,module,exports){ +},{"../../service/RTC/MediaStreamTypes":130,"../../service/RTC/RTCEvents.js":131,"../../service/desktopsharing/DesktopSharingEventTypes":134,"./DataChannels":12,"./JitsiLocalTrack.js":13,"./JitsiRemoteTrack.js":14,"./JitsiTrack":15,"./RTCBrowserType":17,"./RTCUtils.js":18,"events":51}],17:[function(require,module,exports){ var currentBrowser; @@ -3637,7 +3663,7 @@ var RTCUtils = { module.exports = RTCUtils; }).call(this,"/modules/RTC/RTCUtils.js") -},{"../../JitsiTrackErrors":9,"../../service/RTC/RTCEvents":132,"../../service/RTC/Resolutions":133,"../xmpp/SDPUtil":33,"./RTCBrowserType":17,"./ScreenObtainer":19,"./adapter.screenshare":20,"events":45,"jitsi-meet-logger":49}],19:[function(require,module,exports){ +},{"../../JitsiTrackErrors":9,"../../service/RTC/RTCEvents":131,"../../service/RTC/Resolutions":132,"../xmpp/SDPUtil":34,"./RTCBrowserType":17,"./ScreenObtainer":19,"./adapter.screenshare":20,"events":51,"jitsi-meet-logger":79}],19:[function(require,module,exports){ (function (__filename){ /* global chrome, $, alert */ /* jshint -W003 */ @@ -4053,7 +4079,7 @@ function initFirefoxExtensionDetection(options) { module.exports = ScreenObtainer; }).call(this,"/modules/RTC/ScreenObtainer.js") -},{"../../JitsiTrackErrors":9,"../../service/desktopsharing/DesktopSharingEventTypes":135,"./RTCBrowserType":17,"./adapter.screenshare":20,"jitsi-meet-logger":49}],20:[function(require,module,exports){ +},{"../../JitsiTrackErrors":9,"../../service/desktopsharing/DesktopSharingEventTypes":134,"./RTCBrowserType":17,"./adapter.screenshare":20,"jitsi-meet-logger":79}],20:[function(require,module,exports){ (function (__filename){ /*! adapterjs - v0.12.3 - 2015-11-16 */ var console = require("jitsi-meet-logger").getLogger(__filename); @@ -5237,7 +5263,7 @@ if (navigator.mozGetUserMedia) { } }).call(this,"/modules/RTC/adapter.screenshare.js") -},{"jitsi-meet-logger":49}],21:[function(require,module,exports){ +},{"jitsi-meet-logger":79}],21:[function(require,module,exports){ (function (__filename){ var logger = require("jitsi-meet-logger").getLogger(__filename); @@ -5326,7 +5352,7 @@ Settings.prototype.getCallStatsUserName = function () { module.exports = Settings; }).call(this,"/modules/settings/Settings.js") -},{"../util/UsernameGenerator":27,"jitsi-meet-logger":49}],22:[function(require,module,exports){ +},{"../util/UsernameGenerator":28,"jitsi-meet-logger":79}],22:[function(require,module,exports){ (function (__filename){ /* global $, Strophe, callstats */ var logger = require("jitsi-meet-logger").getLogger(__filename); @@ -5587,7 +5613,7 @@ CallStats.sendAddIceCandidateFailed = _try_catch(function (e, pc, cs) { module.exports = CallStats; }).call(this,"/modules/statistics/CallStats.js") -},{"jitsi-meet-logger":49,"jssha":50,"socket.io-client":81}],23:[function(require,module,exports){ +},{"jitsi-meet-logger":79,"jssha":81,"socket.io-client":118}],23:[function(require,module,exports){ /* global config */ /** * Provides statistics for the local stream. @@ -6444,13 +6470,14 @@ StatsCollector.prototype.processAudioLevelReport = function () { }; }).call(this,"/modules/statistics/RTPStatsCollector.js") -},{"../../service/statistics/Events":136,"../RTC/RTCBrowserType":17,"jitsi-meet-logger":49}],25:[function(require,module,exports){ +},{"../../service/statistics/Events":135,"../RTC/RTCBrowserType":17,"jitsi-meet-logger":79}],25:[function(require,module,exports){ /* global require, APP */ var LocalStats = require("./LocalStatsCollector.js"); var RTPStats = require("./RTPStatsCollector.js"); var EventEmitter = require("events"); var StatisticsEvents = require("../../service/statistics/Events"); var CallStats = require("./CallStats"); +var ScriptUtil = require('../util/ScriptUtil'); // Since callstats.io is a third party, we cannot guarantee the quality of // their service. More specifically, their server may take noticeably long @@ -6460,15 +6487,10 @@ var CallStats = require("./CallStats"); // start downloading their API as soon as possible and (2) do the // downloading asynchronously. function loadCallStatsAPI() { - (function (d, src) { - var elementName = 'script'; - var newScript = d.createElement(elementName); - var referenceNode = d.getElementsByTagName(elementName)[0]; - - newScript.async = true; - newScript.src = src; - referenceNode.parentNode.insertBefore(newScript, referenceNode); - })(document, 'https://api.callstats.io/static/callstats.min.js'); + ScriptUtil.loadScript( + 'https://api.callstats.io/static/callstats.min.js', + /* async */ true, + /* prepend */ true); // FIXME At the time of this writing, we hope that the callstats.io API will // have loaded by the time we needed it (i.e. CallStats.init is invoked). } @@ -6728,7 +6750,7 @@ Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID; module.exports = Statistics; -},{"../../service/statistics/Events":136,"../../service/statistics/constants":137,"./CallStats":22,"./LocalStatsCollector.js":23,"./RTPStatsCollector.js":24,"events":45}],26:[function(require,module,exports){ +},{"../../service/statistics/Events":135,"../../service/statistics/constants":136,"../util/ScriptUtil":27,"./CallStats":22,"./LocalStatsCollector.js":23,"./RTPStatsCollector.js":24,"events":51}],26:[function(require,module,exports){ /** /** * @const @@ -6805,6 +6827,40 @@ var RandomUtil = { module.exports = RandomUtil; },{}],27:[function(require,module,exports){ +/** + * Implements utility functions which facilitate the dealing with scripts such + * as the download and execution of a JavaScript file. + */ +var ScriptUtil = { + /** + * Loads a script from a specific source. + * + * @param src the source from the which the script is to be (down)loaded + * @param async true to asynchronously load the script or false to + * synchronously load the script + * @param prepend true to schedule the loading of the script as soon as + * possible or false to schedule the loading of the script at the end of the + * scripts known at the time + */ + loadScript: function (src, async, prepend) { + var d = document; + var tagName = 'script'; + var script = d.createElement(tagName); + var referenceNode = d.getElementsByTagName(tagName)[0]; + + script.async = async; + script.src = src; + if (prepend) { + referenceNode.parentNode.insertBefore(script, referenceNode); + } else { + referenceNode.parentNode.appendChild(script); + } + }, +}; + +module.exports = ScriptUtil; + +},{}],28:[function(require,module,exports){ var RandomUtil = require('./RandomUtil'); /** @@ -7235,7 +7291,7 @@ module.exports = { generateUsername: generateUsername }; -},{"./RandomUtil":26}],28:[function(require,module,exports){ +},{"./RandomUtil":26}],29:[function(require,module,exports){ (function (__filename){ /* global Strophe, $, $pres, $iq, $msg */ /* jshint -W101,-W069 */ @@ -7392,7 +7448,21 @@ ChatRoom.prototype.doLeave = function () { logger.log("do leave", this.myroomjid); var pres = $pres({to: this.myroomjid, type: 'unavailable' }); this.presMap.length = 0; + + // XXX Strophe is asynchronously sending by default. Unfortunately, that + // means that there may not be enough time to send the unavailable presence. + // Switching Strophe to synchronous sending is not much of an option because + // it may lead to a noticeable delay in navigating away from the current + // location. As a compromise, we will try to increase the chances of sending + // the unavailable presence within the short time span that we have upon + // unloading by invoking flush() on the connection. We flush() once before + // sending/queuing the unavailable presence in order to attemtp to have the + // unavailable presence at the top of the send queue. We flush() once more + // after sending/queuing the unavailable presence in order to attempt to + // have it sent as soon as possible. + this.connection.flush(); this.connection.send(pres); + this.connection.flush(); }; @@ -8048,7 +8118,7 @@ ChatRoom.prototype.onMute = function (iq) { module.exports = ChatRoom; }).call(this,"/modules/xmpp/ChatRoom.js") -},{"../../service/xmpp/XMPPEvents":138,"./moderator":35,"./recording":36,"events":45,"jitsi-meet-logger":49}],29:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"./moderator":36,"./recording":37,"events":51,"jitsi-meet-logger":79}],30:[function(require,module,exports){ (function (__filename){ /* * JingleSession provides an API to manage a single Jingle session. We will @@ -8184,7 +8254,7 @@ JingleSession.prototype.setAnswer = function(jingle) {}; module.exports = JingleSession; }).call(this,"/modules/xmpp/JingleSession.js") -},{"jitsi-meet-logger":49}],30:[function(require,module,exports){ +},{"jitsi-meet-logger":79}],31:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ @@ -9847,7 +9917,7 @@ JingleSessionPC.prototype.getIceConnectionState = function () { module.exports = JingleSessionPC; }).call(this,"/modules/xmpp/JingleSessionPC.js") -},{"../../service/xmpp/XMPPEvents":138,"../RTC/RTC":16,"../RTC/RTCBrowserType":17,"./JingleSession":29,"./SDP":31,"./SDPDiffer":32,"./SDPUtil":33,"./TraceablePeerConnection":34,"async":44,"jitsi-meet-logger":49,"sdp-transform":78}],31:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"../RTC/RTC":16,"../RTC/RTCBrowserType":17,"./JingleSession":30,"./SDP":32,"./SDPDiffer":33,"./SDPUtil":34,"./TraceablePeerConnection":35,"async":47,"jitsi-meet-logger":79,"sdp-transform":115}],32:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ @@ -10498,7 +10568,7 @@ SDP.prototype.jingle2media = function (content) { module.exports = SDP; }).call(this,"/modules/xmpp/SDP.js") -},{"./SDPUtil":33,"jitsi-meet-logger":49}],32:[function(require,module,exports){ +},{"./SDPUtil":34,"jitsi-meet-logger":79}],33:[function(require,module,exports){ var SDPUtil = require("./SDPUtil"); function SDPDiffer(mySDP, otherSDP) @@ -10667,7 +10737,7 @@ SDPDiffer.prototype.toJingle = function(modify) { }; module.exports = SDPDiffer; -},{"./SDPUtil":33}],33:[function(require,module,exports){ +},{"./SDPUtil":34}],34:[function(require,module,exports){ (function (__filename){ var logger = require("jitsi-meet-logger").getLogger(__filename); var RTCBrowserType = require("../RTC/RTCBrowserType"); @@ -11035,7 +11105,7 @@ SDPUtil = { module.exports = SDPUtil; }).call(this,"/modules/xmpp/SDPUtil.js") -},{"../RTC/RTCBrowserType":17,"jitsi-meet-logger":49}],34:[function(require,module,exports){ +},{"../RTC/RTCBrowserType":17,"jitsi-meet-logger":79}],35:[function(require,module,exports){ (function (__filename){ /* global $ */ var RTC = require('../RTC/RTC'); @@ -11542,7 +11612,7 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) { module.exports = TraceablePeerConnection; }).call(this,"/modules/xmpp/TraceablePeerConnection.js") -},{"../../service/xmpp/XMPPEvents":138,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"../util/RandomUtil":26,"jitsi-meet-logger":49,"sdp-interop":68,"sdp-simulcast":75,"sdp-transform":78}],35:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"../util/RandomUtil":26,"jitsi-meet-logger":79,"sdp-interop":105,"sdp-simulcast":112,"sdp-transform":115}],36:[function(require,module,exports){ (function (__filename){ /* global $, $iq, Promise, Strophe */ @@ -11998,7 +12068,7 @@ Moderator.prototype.logout = function (callback) { module.exports = Moderator; }).call(this,"/modules/xmpp/moderator.js") -},{"../../service/authentication/AuthenticationEvents":134,"../../service/xmpp/XMPPEvents":138,"jitsi-meet-logger":49}],36:[function(require,module,exports){ +},{"../../service/authentication/AuthenticationEvents":133,"../../service/xmpp/XMPPEvents":137,"jitsi-meet-logger":79}],37:[function(require,module,exports){ (function (__filename){ /* global $, $iq, config, connection, focusMucJid, messageHandler, Toolbar, Util, Promise */ @@ -12225,7 +12295,7 @@ Recording.prototype.getURL = function () { module.exports = Recording; }).call(this,"/modules/xmpp/recording.js") -},{"../../service/xmpp/XMPPEvents":138,"jitsi-meet-logger":49}],37:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"jitsi-meet-logger":79}],38:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ /* a simple MUC connection plugin @@ -12338,7 +12408,7 @@ module.exports = function(XMPP) { }; }).call(this,"/modules/xmpp/strophe.emuc.js") -},{"./ChatRoom":28,"jitsi-meet-logger":49}],38:[function(require,module,exports){ +},{"./ChatRoom":29,"jitsi-meet-logger":79}],39:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ @@ -12635,7 +12705,7 @@ module.exports = function(XMPP, eventEmitter) { }).call(this,"/modules/xmpp/strophe.jingle.js") -},{"../../service/xmpp/XMPPEvents":138,"../RTC/RTCBrowserType":17,"./JingleSessionPC":30,"jitsi-meet-logger":49}],39:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"../RTC/RTCBrowserType":17,"./JingleSessionPC":31,"jitsi-meet-logger":79}],40:[function(require,module,exports){ /* global Strophe */ module.exports = function () { @@ -12656,7 +12726,7 @@ module.exports = function () { } }); }; -},{}],40:[function(require,module,exports){ +},{}],41:[function(require,module,exports){ (function (__filename){ /* global $, $iq, Strophe */ @@ -12783,7 +12853,7 @@ module.exports = function (XMPP, eventEmitter) { }; }).call(this,"/modules/xmpp/strophe.ping.js") -},{"../../service/xmpp/XMPPEvents":138,"jitsi-meet-logger":49}],41:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":137,"jitsi-meet-logger":79}],42:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ var logger = require("jitsi-meet-logger").getLogger(__filename); @@ -12900,7 +12970,7 @@ module.exports = function() { }; }).call(this,"/modules/xmpp/strophe.rayo.js") -},{"jitsi-meet-logger":49}],42:[function(require,module,exports){ +},{"jitsi-meet-logger":79}],43:[function(require,module,exports){ (function (__filename){ /* global Strophe */ /** @@ -12949,7 +13019,7 @@ module.exports = function () { }; }).call(this,"/modules/xmpp/strophe.util.js") -},{"jitsi-meet-logger":49}],43:[function(require,module,exports){ +},{"jitsi-meet-logger":79}],44:[function(require,module,exports){ (function (__filename){ /* global $, APP, config, Strophe*/ @@ -13247,21 +13317,96 @@ XMPP.prototype.getSessions = function () { }; XMPP.prototype.disconnect = function () { - if (this.disconnectInProgress || !this.connection || !this.connection.connected) - { + if (this.disconnectInProgress + || !this.connection + || !this.connection.connected) { this.eventEmitter.emit(JitsiConnectionEvents.WRONG_STATE); return; } this.disconnectInProgress = true; + // XXX Strophe is asynchronously sending by default. Unfortunately, that + // means that there may not be enough time to send an unavailable presence + // or disconnect at all. Switching Strophe to synchronous sending is not + // much of an option because it may lead to a noticeable delay in navigating + // away from the current location. As a compromise, we will try to increase + // the chances of sending an unavailable presence and/or disconecting within + // the short time span that we have upon unloading by invoking flush() on + // the connection. We flush() once before disconnect() in order to attemtp + // to have its unavailable presence at the top of the send queue. We flush() + // once more after disconnect() in order to attempt to have its unavailable + // presence sent as soon as possible. + this.connection.flush(); this.connection.disconnect(); + this.connection.flush(); }; module.exports = XMPP; }).call(this,"/modules/xmpp/xmpp.js") -},{"../../JitsiConnectionErrors":5,"../../JitsiConnectionEvents":6,"../../service/RTC/RTCEvents":132,"../../service/xmpp/XMPPEvents":138,"../RTC/RTC":16,"./strophe.emuc":37,"./strophe.jingle":38,"./strophe.logger":39,"./strophe.ping":40,"./strophe.rayo":41,"./strophe.util":42,"events":45,"jitsi-meet-logger":49,"pako":51}],44:[function(require,module,exports){ +},{"../../JitsiConnectionErrors":5,"../../JitsiConnectionEvents":6,"../../service/RTC/RTCEvents":131,"../../service/xmpp/XMPPEvents":137,"../RTC/RTC":16,"./strophe.emuc":38,"./strophe.jingle":39,"./strophe.logger":40,"./strophe.ping":41,"./strophe.rayo":42,"./strophe.util":43,"events":51,"jitsi-meet-logger":79,"pako":84}],45:[function(require,module,exports){ +module.exports = after + +function after(count, callback, err_cb) { + var bail = false + err_cb = err_cb || noop + proxy.count = count + + return (count === 0) ? callback() : proxy + + function proxy(err, result) { + if (proxy.count <= 0) { + throw new Error('after called too many times') + } + --proxy.count + + // after first error, rest are passed to err_cb + if (err) { + bail = true + callback(err) + // future error callbacks will go to error handler + callback = err_cb + } else if (proxy.count === 0 && !bail) { + callback(null, result) + } + } +} + +function noop() {} + +},{}],46:[function(require,module,exports){ +/** + * An abstraction for slicing an arraybuffer even when + * ArrayBuffer.prototype.slice is not supported + * + * @api public + */ + +module.exports = function(arraybuffer, start, end) { + var bytes = arraybuffer.byteLength; + start = start || 0; + end = end || bytes; + + if (arraybuffer.slice) { return arraybuffer.slice(start, end); } + + if (start < 0) { start += bytes; } + if (end < 0) { end += bytes; } + if (end > bytes) { end = bytes; } + + if (start >= bytes || start >= end || bytes === 0) { + return new ArrayBuffer(0); + } + + var abv = new Uint8Array(arraybuffer); + var result = new Uint8Array(end - start); + for (var i = start, ii = 0; i < end; i++, ii++) { + result[ii] = abv[i]; + } + return result.buffer; +}; + +},{}],47:[function(require,module,exports){ (function (process){ /*! * async @@ -14388,7 +14533,208 @@ module.exports = XMPP; }()); }).call(this,require('_process')) -},{"_process":46}],45:[function(require,module,exports){ +},{"_process":103}],48:[function(require,module,exports){ + +/** + * Expose `Backoff`. + */ + +module.exports = Backoff; + +/** + * Initialize backoff timer with `opts`. + * + * - `min` initial timeout in milliseconds [100] + * - `max` max timeout [10000] + * - `jitter` [0] + * - `factor` [2] + * + * @param {Object} opts + * @api public + */ + +function Backoff(opts) { + opts = opts || {}; + this.ms = opts.min || 100; + this.max = opts.max || 10000; + this.factor = opts.factor || 2; + this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; + this.attempts = 0; +} + +/** + * Return the backoff duration. + * + * @return {Number} + * @api public + */ + +Backoff.prototype.duration = function(){ + var ms = this.ms * Math.pow(this.factor, this.attempts++); + if (this.jitter) { + var rand = Math.random(); + var deviation = Math.floor(rand * this.jitter * ms); + ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; + } + return Math.min(ms, this.max) | 0; +}; + +/** + * Reset the number of attempts. + * + * @api public + */ + +Backoff.prototype.reset = function(){ + this.attempts = 0; +}; + +/** + * Set the minimum duration + * + * @api public + */ + +Backoff.prototype.setMin = function(min){ + this.ms = min; +}; + +/** + * Set the maximum duration + * + * @api public + */ + +Backoff.prototype.setMax = function(max){ + this.max = max; +}; + +/** + * Set the jitter + * + * @api public + */ + +Backoff.prototype.setJitter = function(jitter){ + this.jitter = jitter; +}; + + +},{}],49:[function(require,module,exports){ +/* + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ +(function(chars){ + "use strict"; + + exports.encode = function(arraybuffer) { + var bytes = new Uint8Array(arraybuffer), + i, len = bytes.length, base64 = ""; + + for (i = 0; i < len; i+=3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + + if ((len % 3) === 2) { + base64 = base64.substring(0, base64.length - 1) + "="; + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + "=="; + } + + return base64; + }; + + exports.decode = function(base64) { + var bufferLength = base64.length * 0.75, + len = base64.length, i, p = 0, + encoded1, encoded2, encoded3, encoded4; + + if (base64[base64.length - 1] === "=") { + bufferLength--; + if (base64[base64.length - 2] === "=") { + bufferLength--; + } + } + + var arraybuffer = new ArrayBuffer(bufferLength), + bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i+=4) { + encoded1 = chars.indexOf(base64[i]); + encoded2 = chars.indexOf(base64[i+1]); + encoded3 = chars.indexOf(base64[i+2]); + encoded4 = chars.indexOf(base64[i+3]); + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; + }; +})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); + +},{}],50:[function(require,module,exports){ +(function (global){ +/** + * Create a blob builder even when vendor prefixes exist + */ + +var BlobBuilder = global.BlobBuilder + || global.WebKitBlobBuilder + || global.MSBlobBuilder + || global.MozBlobBuilder; + +/** + * Check if Blob constructor is supported + */ + +var blobSupported = (function() { + try { + var b = new Blob(['hi']); + return b.size == 2; + } catch(e) { + return false; + } +})(); + +/** + * Check if BlobBuilder is supported + */ + +var blobBuilderSupported = BlobBuilder + && BlobBuilder.prototype.append + && BlobBuilder.prototype.getBlob; + +function BlobBuilderConstructor(ary, options) { + options = options || {}; + + var bb = new BlobBuilder(); + for (var i = 0; i < ary.length; i++) { + bb.append(ary[i]); + } + return (options.type) ? bb.getBlob(options.type) : bb.getBlob(); +}; + +module.exports = (function() { + if (blobSupported) { + return global.Blob; + } else if (blobBuilderSupported) { + return BlobBuilderConstructor; + } else { + return undefined; + } +})(); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],51:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -14691,100 +15037,3508 @@ function isUndefined(arg) { return arg === void 0; } -},{}],46:[function(require,module,exports){ -// shim for using process in browser +},{}],52:[function(require,module,exports){ +/** + * Slice reference. + */ -var process = module.exports = {}; -var queue = []; -var draining = false; -var currentQueue; -var queueIndex = -1; +var slice = [].slice; -function cleanUpNextTick() { - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); +/** + * Bind `obj` to `fn`. + * + * @param {Object} obj + * @param {Function|String} fn or string + * @return {Function} + * @api public + */ + +module.exports = function(obj, fn){ + if ('string' == typeof fn) fn = obj[fn]; + if ('function' != typeof fn) throw new Error('bind() requires a function'); + var args = slice.call(arguments, 2); + return function(){ + return fn.apply(obj, args.concat(slice.call(arguments))); + } +}; + +},{}],53:[function(require,module,exports){ + +/** + * Expose `Emitter`. + */ + +module.exports = Emitter; + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + +function Emitter(obj) { + if (obj) return mixin(obj); +}; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + + function on() { + self.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; +}; + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } + + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } + + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; +}; + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } + + return this; +}; + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; + +},{}],54:[function(require,module,exports){ + +module.exports = function(a, b){ + var fn = function(){}; + fn.prototype = b.prototype; + a.prototype = new fn; + a.prototype.constructor = a; +}; +},{}],55:[function(require,module,exports){ + +/** + * Expose `debug()` as the module. + */ + +module.exports = debug; + +/** + * Create a debugger with the given `name`. + * + * @param {String} name + * @return {Type} + * @api public + */ + +function debug(name) { + if (!debug.enabled(name)) return function(){}; + + return function(fmt){ + fmt = coerce(fmt); + + var curr = new Date; + var ms = curr - (debug[name] || curr); + debug[name] = curr; + + fmt = name + + ' ' + + fmt + + ' +' + debug.humanize(ms); + + // This hackery is required for IE8 + // where `console.log` doesn't have 'apply' + window.console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); + } +} + +/** + * The currently active debug mode names. + */ + +debug.names = []; +debug.skips = []; + +/** + * Enables a debug mode by name. This can include modes + * separated by a colon and wildcards. + * + * @param {String} name + * @api public + */ + +debug.enable = function(name) { + try { + localStorage.debug = name; + } catch(e){} + + var split = (name || '').split(/[\s,]+/) + , len = split.length; + + for (var i = 0; i < len; i++) { + name = split[i].replace('*', '.*?'); + if (name[0] === '-') { + debug.skips.push(new RegExp('^' + name.substr(1) + '$')); + } + else { + debug.names.push(new RegExp('^' + name + '$')); + } + } +}; + +/** + * Disable debug output. + * + * @api public + */ + +debug.disable = function(){ + debug.enable(''); +}; + +/** + * Humanize the given `ms`. + * + * @param {Number} m + * @return {String} + * @api private + */ + +debug.humanize = function(ms) { + var sec = 1000 + , min = 60 * 1000 + , hour = 60 * min; + + if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; + if (ms >= min) return (ms / min).toFixed(1) + 'm'; + if (ms >= sec) return (ms / sec | 0) + 's'; + return ms + 'ms'; +}; + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +debug.enabled = function(name) { + for (var i = 0, len = debug.skips.length; i < len; i++) { + if (debug.skips[i].test(name)) { + return false; + } + } + for (var i = 0, len = debug.names.length; i < len; i++) { + if (debug.names[i].test(name)) { + return true; + } + } + return false; +}; + +/** + * Coerce `val`. + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +// persist + +try { + if (window.localStorage) debug.enable(localStorage.debug); +} catch(e){} + +},{}],56:[function(require,module,exports){ + +module.exports = require('./lib/'); + +},{"./lib/":57}],57:[function(require,module,exports){ + +module.exports = require('./socket'); + +/** + * Exports parser + * + * @api public + * + */ +module.exports.parser = require('engine.io-parser'); + +},{"./socket":58,"engine.io-parser":69}],58:[function(require,module,exports){ +(function (global){ +/** + * Module dependencies. + */ + +var transports = require('./transports'); +var Emitter = require('component-emitter'); +var debug = require('debug')('engine.io-client:socket'); +var index = require('indexof'); +var parser = require('engine.io-parser'); +var parseuri = require('parseuri'); +var parsejson = require('parsejson'); +var parseqs = require('parseqs'); + +/** + * Module exports. + */ + +module.exports = Socket; + +/** + * Noop function. + * + * @api private + */ + +function noop(){} + +/** + * Socket constructor. + * + * @param {String|Object} uri or options + * @param {Object} options + * @api public + */ + +function Socket(uri, opts){ + if (!(this instanceof Socket)) return new Socket(uri, opts); + + opts = opts || {}; + + if (uri && 'object' == typeof uri) { + opts = uri; + uri = null; + } + + if (uri) { + uri = parseuri(uri); + opts.host = uri.host; + opts.secure = uri.protocol == 'https' || uri.protocol == 'wss'; + opts.port = uri.port; + if (uri.query) opts.query = uri.query; + } + + this.secure = null != opts.secure ? opts.secure : + (global.location && 'https:' == location.protocol); + + if (opts.host) { + var pieces = opts.host.split(':'); + opts.hostname = pieces.shift(); + if (pieces.length) { + opts.port = pieces.pop(); + } else if (!opts.port) { + // if no port is specified manually, use the protocol default + opts.port = this.secure ? '443' : '80'; + } + } + + this.agent = opts.agent || false; + this.hostname = opts.hostname || + (global.location ? location.hostname : 'localhost'); + this.port = opts.port || (global.location && location.port ? + location.port : + (this.secure ? 443 : 80)); + this.query = opts.query || {}; + if ('string' == typeof this.query) this.query = parseqs.decode(this.query); + this.upgrade = false !== opts.upgrade; + this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; + this.forceJSONP = !!opts.forceJSONP; + this.jsonp = false !== opts.jsonp; + this.forceBase64 = !!opts.forceBase64; + this.enablesXDR = !!opts.enablesXDR; + this.timestampParam = opts.timestampParam || 't'; + this.timestampRequests = opts.timestampRequests; + this.transports = opts.transports || ['polling', 'websocket']; + this.readyState = ''; + this.writeBuffer = []; + this.callbackBuffer = []; + this.policyPort = opts.policyPort || 843; + this.rememberUpgrade = opts.rememberUpgrade || false; + this.binaryType = null; + this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; + + // SSL options for Node.js client + this.pfx = opts.pfx || null; + this.key = opts.key || null; + this.passphrase = opts.passphrase || null; + this.cert = opts.cert || null; + this.ca = opts.ca || null; + this.ciphers = opts.ciphers || null; + this.rejectUnauthorized = opts.rejectUnauthorized || null; + + this.open(); +} + +Socket.priorWebsocketSuccess = false; + +/** + * Mix in `Emitter`. + */ + +Emitter(Socket.prototype); + +/** + * Protocol version. + * + * @api public + */ + +Socket.protocol = parser.protocol; // this is an int + +/** + * Expose deps for legacy compatibility + * and standalone browser access. + */ + +Socket.Socket = Socket; +Socket.Transport = require('./transport'); +Socket.transports = require('./transports'); +Socket.parser = require('engine.io-parser'); + +/** + * Creates transport of the given type. + * + * @param {String} transport name + * @return {Transport} + * @api private + */ + +Socket.prototype.createTransport = function (name) { + debug('creating transport "%s"', name); + var query = clone(this.query); + + // append engine.io protocol identifier + query.EIO = parser.protocol; + + // transport name + query.transport = name; + + // session id if we already have one + if (this.id) query.sid = this.id; + + var transport = new transports[name]({ + agent: this.agent, + hostname: this.hostname, + port: this.port, + secure: this.secure, + path: this.path, + query: query, + forceJSONP: this.forceJSONP, + jsonp: this.jsonp, + forceBase64: this.forceBase64, + enablesXDR: this.enablesXDR, + timestampRequests: this.timestampRequests, + timestampParam: this.timestampParam, + policyPort: this.policyPort, + socket: this, + pfx: this.pfx, + key: this.key, + passphrase: this.passphrase, + cert: this.cert, + ca: this.ca, + ciphers: this.ciphers, + rejectUnauthorized: this.rejectUnauthorized + }); + + return transport; +}; + +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} + +/** + * Initializes transport to use and starts probe. + * + * @api private + */ +Socket.prototype.open = function () { + var transport; + if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { + transport = 'websocket'; + } else if (0 == this.transports.length) { + // Emit error on next tick so it can be listened to + var self = this; + setTimeout(function() { + self.emit('error', 'No transports available'); + }, 0); + return; + } else { + transport = this.transports[0]; + } + this.readyState = 'opening'; + + // Retry with the next transport if the transport is disabled (jsonp: false) + var transport; + try { + transport = this.createTransport(transport); + } catch (e) { + this.transports.shift(); + this.open(); + return; + } + + transport.open(); + this.setTransport(transport); +}; + +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ + +Socket.prototype.setTransport = function(transport){ + debug('setting transport %s', transport.name); + var self = this; + + if (this.transport) { + debug('clearing existing transport %s', this.transport.name); + this.transport.removeAllListeners(); + } + + // set up transport + this.transport = transport; + + // set up transport listeners + transport + .on('drain', function(){ + self.onDrain(); + }) + .on('packet', function(packet){ + self.onPacket(packet); + }) + .on('error', function(e){ + self.onError(e); + }) + .on('close', function(){ + self.onClose('transport close'); + }); +}; + +/** + * Probes a transport. + * + * @param {String} transport name + * @api private + */ + +Socket.prototype.probe = function (name) { + debug('probing transport "%s"', name); + var transport = this.createTransport(name, { probe: 1 }) + , failed = false + , self = this; + + Socket.priorWebsocketSuccess = false; + + function onTransportOpen(){ + if (self.onlyBinaryUpgrades) { + var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; + failed = failed || upgradeLosesBinary; + } + if (failed) return; + + debug('probe transport "%s" opened', name); + transport.send([{ type: 'ping', data: 'probe' }]); + transport.once('packet', function (msg) { + if (failed) return; + if ('pong' == msg.type && 'probe' == msg.data) { + debug('probe transport "%s" pong', name); + self.upgrading = true; + self.emit('upgrading', transport); + if (!transport) return; + Socket.priorWebsocketSuccess = 'websocket' == transport.name; + + debug('pausing current transport "%s"', self.transport.name); + self.transport.pause(function () { + if (failed) return; + if ('closed' == self.readyState) return; + debug('changing transport and sending upgrade packet'); + + cleanup(); + + self.setTransport(transport); + transport.send([{ type: 'upgrade' }]); + self.emit('upgrade', transport); + transport = null; + self.upgrading = false; + self.flush(); + }); + } else { + debug('probe transport "%s" failed', name); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('upgradeError', err); + } + }); + } + + function freezeTransport() { + if (failed) return; + + // Any callback called by transport should be ignored since now + failed = true; + + cleanup(); + + transport.close(); + transport = null; + } + + //Handle any error that happens while probing + function onerror(err) { + var error = new Error('probe error: ' + err); + error.transport = transport.name; + + freezeTransport(); + + debug('probe transport "%s" failed because of error: %s', name, err); + + self.emit('upgradeError', error); + } + + function onTransportClose(){ + onerror("transport closed"); + } + + //When the socket is closed while we're probing + function onclose(){ + onerror("socket closed"); + } + + //When the socket is upgraded while we're probing + function onupgrade(to){ + if (transport && to.name != transport.name) { + debug('"%s" works - aborting "%s"', to.name, transport.name); + freezeTransport(); + } + } + + //Remove all listeners on the transport and on self + function cleanup(){ + transport.removeListener('open', onTransportOpen); + transport.removeListener('error', onerror); + transport.removeListener('close', onTransportClose); + self.removeListener('close', onclose); + self.removeListener('upgrading', onupgrade); + } + + transport.once('open', onTransportOpen); + transport.once('error', onerror); + transport.once('close', onTransportClose); + + this.once('close', onclose); + this.once('upgrading', onupgrade); + + transport.open(); + +}; + +/** + * Called when connection is deemed open. + * + * @api public + */ + +Socket.prototype.onOpen = function () { + debug('socket open'); + this.readyState = 'open'; + Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; + this.emit('open'); + this.flush(); + + // we check for `readyState` in case an `open` + // listener already closed the socket + if ('open' == this.readyState && this.upgrade && this.transport.pause) { + debug('starting upgrade probes'); + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); + } + } +}; + +/** + * Handles a packet. + * + * @api private + */ + +Socket.prototype.onPacket = function (packet) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); + + this.emit('packet', packet); + + // Socket is live - any packet counts + this.emit('heartbeat'); + + switch (packet.type) { + case 'open': + this.onHandshake(parsejson(packet.data)); + break; + + case 'pong': + this.setPing(); + break; + + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.emit('error', err); + break; + + case 'message': + this.emit('data', packet.data); + this.emit('message', packet.data); + break; + } + } else { + debug('packet received with socket readyState "%s"', this.readyState); + } +}; + +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ + +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.id = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = this.filterUpgrades(data.upgrades); + this.pingInterval = data.pingInterval; + this.pingTimeout = data.pingTimeout; + this.onOpen(); + // In case open handler closes socket + if ('closed' == this.readyState) return; + this.setPing(); + + // Prolong liveness of socket on heartbeat + this.removeListener('heartbeat', this.onHeartbeat); + this.on('heartbeat', this.onHeartbeat); +}; + +/** + * Resets ping timeout. + * + * @api private + */ + +Socket.prototype.onHeartbeat = function (timeout) { + clearTimeout(this.pingTimeoutTimer); + var self = this; + self.pingTimeoutTimer = setTimeout(function () { + if ('closed' == self.readyState) return; + self.onClose('ping timeout'); + }, timeout || (self.pingInterval + self.pingTimeout)); +}; + +/** + * Pings server every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @api private + */ + +Socket.prototype.setPing = function () { + var self = this; + clearTimeout(self.pingIntervalTimer); + self.pingIntervalTimer = setTimeout(function () { + debug('writing ping packet - expecting pong within %sms', self.pingTimeout); + self.ping(); + self.onHeartbeat(self.pingTimeout); + }, self.pingInterval); +}; + +/** +* Sends a ping packet. +* +* @api public +*/ + +Socket.prototype.ping = function () { + this.sendPacket('ping'); +}; + +/** + * Called on `drain` event + * + * @api private + */ + +Socket.prototype.onDrain = function() { + for (var i = 0; i < this.prevBufferLen; i++) { + if (this.callbackBuffer[i]) { + this.callbackBuffer[i](); + } + } + + this.writeBuffer.splice(0, this.prevBufferLen); + this.callbackBuffer.splice(0, this.prevBufferLen); + + // setting prevBufferLen = 0 is very important + // for example, when upgrading, upgrade packet is sent over, + // and a nonzero prevBufferLen could cause problems on `drain` + this.prevBufferLen = 0; + + if (this.writeBuffer.length == 0) { + this.emit('drain'); + } else { + this.flush(); + } +}; + +/** + * Flush write buffers. + * + * @api private + */ + +Socket.prototype.flush = function () { + if ('closed' != this.readyState && this.transport.writable && + !this.upgrading && this.writeBuffer.length) { + debug('flushing %d packets in socket', this.writeBuffer.length); + this.transport.send(this.writeBuffer); + // keep track of current length of writeBuffer + // splice writeBuffer and callbackBuffer on `drain` + this.prevBufferLen = this.writeBuffer.length; + this.emit('flush'); + } +}; + +/** + * Sends a message. + * + * @param {String} message. + * @param {Function} callback function. + * @return {Socket} for chaining. + * @api public + */ + +Socket.prototype.write = +Socket.prototype.send = function (msg, fn) { + this.sendPacket('message', msg, fn); + return this; +}; + +/** + * Sends a packet. + * + * @param {String} packet type. + * @param {String} data. + * @param {Function} callback function. + * @api private + */ + +Socket.prototype.sendPacket = function (type, data, fn) { + if ('closing' == this.readyState || 'closed' == this.readyState) { + return; + } + + var packet = { type: type, data: data }; + this.emit('packetCreate', packet); + this.writeBuffer.push(packet); + this.callbackBuffer.push(fn); + this.flush(); +}; + +/** + * Closes the connection. + * + * @api private + */ + +Socket.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.readyState = 'closing'; + + var self = this; + + function close() { + self.onClose('forced close'); + debug('socket closing - telling transport to close'); + self.transport.close(); + } + + function cleanupAndClose() { + self.removeListener('upgrade', cleanupAndClose); + self.removeListener('upgradeError', cleanupAndClose); + close(); + } + + function waitForUpgrade() { + // wait for upgrade to finish since we can't send packets while pausing a transport + self.once('upgrade', cleanupAndClose); + self.once('upgradeError', cleanupAndClose); + } + + if (this.writeBuffer.length) { + this.once('drain', function() { + if (this.upgrading) { + waitForUpgrade(); + } else { + close(); + } + }); + } else if (this.upgrading) { + waitForUpgrade(); } else { - queueIndex = -1; - } - if (queue.length) { - drainQueue(); + close(); } + } + + return this; +}; + +/** + * Called upon transport error + * + * @api private + */ + +Socket.prototype.onError = function (err) { + debug('socket error %j', err); + Socket.priorWebsocketSuccess = false; + this.emit('error', err); + this.onClose('transport error', err); +}; + +/** + * Called upon transport close. + * + * @api private + */ + +Socket.prototype.onClose = function (reason, desc) { + if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) { + debug('socket close with reason: "%s"', reason); + var self = this; + + // clear timers + clearTimeout(this.pingIntervalTimer); + clearTimeout(this.pingTimeoutTimer); + + // clean buffers in next tick, so developers can still + // grab the buffers on `close` event + setTimeout(function() { + self.writeBuffer = []; + self.callbackBuffer = []; + self.prevBufferLen = 0; + }, 0); + + // stop event from firing again for transport + this.transport.removeAllListeners('close'); + + // ensure transport won't stay open + this.transport.close(); + + // ignore further transport communication + this.transport.removeAllListeners(); + + // set ready state + this.readyState = 'closed'; + + // clear session id + this.id = null; + + // emit close event + this.emit('close', reason, desc); + } +}; + +/** + * Filters upgrades, returning only those matching client transports. + * + * @param {Array} server upgrades + * @api private + * + */ + +Socket.prototype.filterUpgrades = function (upgrades) { + var filteredUpgrades = []; + for (var i = 0, j = upgrades.length; i