From 8002dcbe665fc37072ba42311b1cf7f6a4a3e4c0 Mon Sep 17 00:00:00 2001 From: isymchych Date: Tue, 1 Dec 2015 15:44:50 +0200 Subject: [PATCH] update lib --- lib-jitsi-meet.js | 22879 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 22879 insertions(+) create mode 100644 lib-jitsi-meet.js diff --git a/lib-jitsi-meet.js b/lib-jitsi-meet.js new file mode 100644 index 000000000..595f42bc8 --- /dev/null +++ b/lib-jitsi-meet.js @@ -0,0 +1,22879 @@ +(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.JitsiMeetJS = 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;otrue objects with the following structure {stream: the Media Stream, + * type: "audio" or "video", videoType: "camera" or "desktop"} + * will be returned trough the Promise, otherwise JitsiTrack objects will be returned. + * @param {string} options.cameraDeviceId + * @param {string} options.micDeviceId + * @returns {Promise.<{Array.}, JitsiConferenceError>} A promise that returns an array of created JitsiTracks if resolved, + * or a JitsiConferenceError if rejected. + */ + createLocalTracks: function (options) { + return RTC.obtainAudioAndVideoPermissions(options || {}); + }, + isDeviceListAvailable: function () { + return RTC.isDeviceListAvailable(); + }, + enumerateDevices: function (callback) { + RTC.enumerateDevices(callback); + } +}; + +require("es6-promise").polyfill() +//Setups the promise object. +window.Promise = window.Promise || require("es6-promise").Promise; + +module.exports = LibJitsiMeet; + +},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiConnection":4,"./JitsiConnectionErrors":5,"./JitsiConnectionEvents":6,"./modules/RTC/RTC":13,"es6-promise":43,"jitsi-meet-logger":45}],8:[function(require,module,exports){ +/** + * Represents a participant in (a member of) a conference. + */ +function JitsiParticipant(id, conference, displayName){ + this._id = id; + this._conference = conference; + this._displayName = displayName; +} + +/** + * @returns {JitsiConference} The conference that this participant belongs to. + */ +JitsiParticipant.prototype.getConference = function() { + return this._conference; +} + +/** + * @returns {Array.} The list of media tracks for this participant. + */ +JitsiParticipant.prototype.getTracks = function() { + +} + +/** + * @returns {String} The ID (i.e. JID) of this participant. + */ +JitsiParticipant.prototype.getId = function() { + return this._id; +} + +/** + * @returns {String} The human-readable display name of this participant. + */ +JitsiParticipant.prototype.getDisplayName = function() { + return this._displayName; +} + +/** + * @returns {Boolean} Whether this participant is a moderator or not. + */ +JitsiParticipant.prototype.isModerator = function() { +} + +// Gets a link to an etherpad instance advertised by the participant? +//JitsiParticipant.prototype.getEtherpad = function() { +// +//} + + +/* + * @returns {Boolean} Whether this participant has muted their audio. + */ +JitsiParticipant.prototype.isAudioMuted = function() { + +} + +/* + * @returns {Boolean} Whether this participant has muted their video. + */ +JitsiParticipant.prototype.isVideoMuted = function() { + +} + +/* + * @returns {???} The latest statistics reported by this participant (i.e. info used to populate the GSM bars) + * TODO: do we expose this or handle it internally? + */ +JitsiParticipant.prototype.getLatestStats = function() { + +} + +/** + * @returns {String} The role of this participant. + */ +JitsiParticipant.prototype.getRole = function() { + +} + +/* + * @returns {Boolean} Whether this participant is the conference focus (i.e. jicofo). + */ +JitsiParticipant.prototype.isFocus = function() { + +} + +/* + * @returns {Boolean} Whether this participant is a conference recorder (i.e. jirecon). + */ +JitsiParticipant.prototype.isRecorder = function() { + +} + +/* + * @returns {Boolean} Whether this participant is a SIP gateway (i.e. jigasi). + */ +JitsiParticipant.prototype.isSipGateway = function() { + +} + +/** + * @returns {String} The ID for this participant's avatar. + */ +JitsiParticipant.prototype.getAvatarId = function() { + +} + +/** + * @returns {Boolean} Whether this participant is currently sharing their screen. + */ +JitsiParticipant.prototype.isScreenSharing = function() { + +} + +/** + * @returns {String} The user agent of this participant (i.e. browser userAgent string). + */ +JitsiParticipant.prototype.getUserAgent = function() { + +} + +/** + * Kicks the participant from the conference (requires certain privileges). + */ +JitsiParticipant.prototype.kick = function() { + +} + +/** + * Asks this participant to mute themselves. + */ +JitsiParticipant.prototype.askToMute = function() { + +} + + +module.exports = JitsiParticipant(); + +},{}],9:[function(require,module,exports){ +(function (__filename){ +/* global config, APP, Strophe */ + +// cache datachannels to avoid garbage collection +// https://code.google.com/p/chromium/issues/detail?id=405545 + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var RTCEvents = require("../../service/RTC/RTCEvents"); + + +/** + * Binds "ondatachannel" event listener to given PeerConnection instance. + * @param peerConnection WebRTC peer connection instance. + */ +function DataChannels(peerConnection, emitter) { + peerConnection.ondatachannel = this.onDataChannel.bind(this); + this.eventEmitter = emitter; + + this._dataChannels = []; + + // 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; + logger.info("Got My Data Channel Message:", msgData, dataChannel); + };*/ +}; + + +/** + * Callback triggered by PeerConnection when new data channel is opened + * on the bridge. + * @param event the event info object. + */ +DataChannels.prototype.onDataChannel = function (event) { + var dataChannel = event.channel; + var self = this; + + dataChannel.onopen = function () { + logger.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)); + + self.eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN); + }; + + dataChannel.onerror = function (error) { + logger.error("Data Channel Error:", error, dataChannel); + }; + + dataChannel.onmessage = function (event) { + var data = event.data; + // JSON + var obj; + + try { + obj = JSON.parse(data); + } + catch (e) { + logger.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; + + logger.info( + "Data channel new dominant speaker event: ", + dominantSpeakerEndpoint); + self.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(); + } + } + + self.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; + + logger.info( + "Data channel new last-n event: ", + lastNEndpoints, endpointsEnteringLastN, obj); + this.eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED, + lastNEndpoints, endpointsEnteringLastN, obj); + } + else { + logger.debug("Data channel JSON-formatted message: ", obj); + } + } + }; + + dataChannel.onclose = function () { + logger.info("The Data Channel closed", dataChannel); + var idx = self._dataChannels.indexOf(dataChannel); + if (idx > -1) + self._dataChannels = self._dataChannels.splice(idx, 1); + }; + this._dataChannels.push(dataChannel); +}; + +DataChannels.prototype.handleSelectedEndpointEvent = function (userResource) { + logger.log('selected endpoint changed: ', userResource); + if (this._dataChannels && this._dataChannels.length != 0) { + this._dataChannels.some(function (dataChannel) { + if (dataChannel.readyState == 'open') { + logger.log('sending selected endpoint changed ' + + 'notification to the bridge: ', userResource); + dataChannel.send(JSON.stringify({ + 'colibriClass': 'SelectedEndpointChangedEvent', + 'selectedEndpoint': + (!userResource || userResource === null)? + null : userResource + })); + + return true; + } + }); + } +} + +DataChannels.prototype.handlePinnedEndpointEvent = function (userResource) { + logger.log('pinned endpoint changed: ', userResource); + if (this._dataChannels && this._dataChannels.length !== 0) { + this._dataChannels.some(function (dataChannel) { + if (dataChannel.readyState == 'open') { + dataChannel.send(JSON.stringify({ + 'colibriClass': 'PinnedEndpointChangedEvent', + 'pinnedEndpoint': + userResource ? userResource : null + })); + + return true; + } + }); + } +} + +module.exports = DataChannels; + + +}).call(this,"/modules/RTC/DataChannels.js") +},{"../../service/RTC/RTCEvents":77,"jitsi-meet-logger":45}],10:[function(require,module,exports){ +var JitsiTrack = require("./JitsiTrack"); +var StreamEventTypes = require("../../service/RTC/StreamEventTypes"); +var RTCBrowserType = require("./RTCBrowserType"); + +/** + * Represents a single media track (either audio or video). + * @constructor + */ +function JitsiLocalTrack(RTC, stream, eventEmitter, videoType, + resolution) +{ + JitsiTrack.call(this, RTC, stream, + function () { + if(!self.dontFireRemoveEvent) + self.eventEmitter.emit( + StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, self); + self.dontFireRemoveEvent = false; + }); + this.eventEmitter = eventEmitter; + this.videoType = videoType; + this.dontFireRemoveEvent = false; + this.resolution = resolution; + var self = this; +} + +JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype); +JitsiLocalTrack.prototype.constructor = JitsiLocalTrack; + +/** + * Mutes / unmutes the track. + * @param mute {boolean} if true the track will be muted. Otherwise the track will be unmuted. + */ +JitsiLocalTrack.prototype._setMute = function (mute) { + if(!this.rtc) { + console.error("Mute is not supported for streams not attached to conference!"); + return; + } + var isAudio = this.type === JitsiTrack.AUDIO; + this.dontFireRemoveEvent = false; + + if ((window.location.protocol != "https:") || + (isAudio) || this.videoType === "screen" || + // FIXME FF does not support 'removeStream' method used to mute + RTCBrowserType.isFirefox()) { + + var tracks = this._getTracks(); + for (var idx = 0; idx < tracks.length; idx++) { + tracks[idx].enabled = !mute; + } + if(isAudio) + this.rtc.room.setAudioMute(mute); + else + this.rtc.room.setVideoMute(mute); + this.eventEmitter.emit(StreamEventTypes.TRACK_MUTE_CHANGED, this); + } else { + if (mute) { + this.dontFireRemoveEvent = true; + this.rtc.room.removeStream(this.stream); + this.rtc.stopMediaStream(this.stream); + if(isAudio) + this.rtc.room.setAudioMute(mute); + else + this.rtc.room.setVideoMute(mute); + this.stream = null; + this.eventEmitter.emit(StreamEventTypes.TRACK_MUTE_CHANGED, this); + //FIXME: Maybe here we should set the SRC for the containers to something + } else { + var self = this; + var RTC = require("./RTCUtils"); + RTC.obtainAudioAndVideoPermissions({ + devices: (isAudio ? ["audio"] : ["video"]), + resolution: self.resolution, + dontCreateJitsiTrack: true}) + .then(function (streams) { + var stream = null; + for(var i = 0; i < streams.length; i++) { + stream = streams[i]; + if(stream.type === self.type) { + self.stream = stream.stream; + self.videoType = stream.videoType; + break; + } + } + + if(!stream) + return; + + for(var i = 0; i < self.containers.length; i++) + { + RTC.attachMediaStream(self.containers[i], self.stream); + } + + self.rtc.room.addStream(stream.stream, + function () { + if(isAudio) + self.rtc.room.setAudioMute(mute); + else + self.rtc.room.setVideoMute(mute); + self.eventEmitter.emit(StreamEventTypes.TRACK_MUTE_CHANGED, self); + }); + }); + } + } +} + +/** + * Stops sending the media track. And removes it from the HTML. + * NOTE: Works for local tracks only. + */ +JitsiLocalTrack.prototype.stop = function () { + if(!this.stream) + return; + if(this.rtc) + this.rtc.room.removeStream(this.stream); + this.rtc.stopMediaStream(this.stream); + this.detach(); +} + +/** + * Returns true - if the stream is muted + * and false otherwise. + * @returns {boolean} true - if the stream is muted + * and false otherwise. + */ +JitsiLocalTrack.prototype.isMuted = function () { + if (!this.stream) + return true; + var tracks = []; + var isAudio = this.type === JitsiTrack.AUDIO; + if (isAudio) { + tracks = this.stream.getAudioTracks(); + } else { + if (!this.isActive()) + return true; + tracks = this.stream.getVideoTracks(); + } + for (var idx = 0; idx < tracks.length; idx++) { + if(tracks[idx].enabled) + return false; + } + return true; +}; + +/** + * Private method. Updates rtc property of the track. + * @param rtc the rtc instance. + */ +JitsiLocalTrack.prototype._setRTC = function (rtc) { + this.rtc = rtc; +}; + +module.exports = JitsiLocalTrack; + +},{"../../service/RTC/StreamEventTypes":79,"./JitsiTrack":12,"./RTCBrowserType":14,"./RTCUtils":15}],11:[function(require,module,exports){ +var JitsiTrack = require("./JitsiTrack"); +var StreamEventTypes = require("../../service/RTC/StreamEventTypes"); + +/** + * Represents a single media track (either audio or video). + * @param RTC the rtc instance. + * @param data object with the stream and some details about it(participant id, video type, etc.) + * @param sid sid for the Media Stream + * @param ssrc ssrc for the Media Stream + * @param eventEmitter the event emitter + * @constructor + */ +function JitsiRemoteTrack(RTC, data, sid, ssrc, eventEmitter) { + JitsiTrack.call(this, RTC, data.stream, + function () { + eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_ENDED, self); + }); + this.rtc = RTC; + this.sid = sid; + this.stream = data.stream; + this.peerjid = data.peerjid; + this.videoType = data.videoType; + this.ssrc = ssrc; + this.muted = false; + if((this.type === JitsiTrack.AUDIO && data.audiomuted) + || (this.type === JitsiTrack.VIDEO && data.videomuted)) { + this.muted = true + } + this.eventEmitter = eventEmitter; + var self = this; +} + +JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype); +JitsiRemoteTrack.prototype.constructor = JitsiRemoteTrack; + +/** + * Sets current muted status and fires an events for the change. + * @param value the muted status. + */ +JitsiRemoteTrack.prototype.setMute = function (value) { + this.stream.muted = value; + this.muted = value; + this.eventEmitter.emit(StreamEventTypes.TRACK_MUTE_CHANGED, this); +}; + +/** + * Returns the current muted status of the track. + * @returns {boolean|*|JitsiRemoteTrack.muted} true if the track is muted and false otherwise. + */ +JitsiRemoteTrack.prototype.isMuted = function () { + return this.muted; +} + +/** + * Returns the participant id which owns the track. + * @returns {string} the id of the participants. + */ +JitsiRemoteTrack.prototype.getParitcipantId = function() { + return Strophe.getResourceFromJid(this.peerjid); +}; + +delete JitsiRemoteTrack.prototype.stop; + +delete JitsiRemoteTrack.prototype.start; + +module.exports = JitsiRemoteTrack; + +},{"../../service/RTC/StreamEventTypes":79,"./JitsiTrack":12}],12:[function(require,module,exports){ +var RTCBrowserType = require("./RTCBrowserType"); + +/** + * This implements 'onended' callback normally fired by WebRTC after the stream + * is stopped. There is no such behaviour yet in FF, so we have to add it. + * @param jitsiTrack our track object holding the original WebRTC stream object + * to which 'onended' handling will be added. + */ +function implementOnEndedHandling(jitsiTrack) { + var stream = jitsiTrack.getOriginalStream(); + var originalStop = stream.stop; + stream.stop = function () { + originalStop.apply(stream); + if (jitsiTrack.isActive()) { + stream.onended(); + } + }; +} + +/** + * Adds onended/oninactive handler to a MediaStream. + * @param mediaStream a MediaStream to attach onended/oninactive handler + * @param handler the handler + */ +function addMediaStreamInactiveHandler(mediaStream, handler) { + if (mediaStream.addEventListener) { + // chrome + if(typeof mediaStream.active !== "undefined") + mediaStream.oninactive = handler; + else + mediaStream.onended = handler; + } else { + // themasys + mediaStream.attachEvent('ended', function () { + handler(mediaStream); + }); + } +} + +/** + * Represents a single media track (either audio or video). + * @constructor + * @param rtc the rtc instance + * @param stream the stream + * @param streamInactiveHandler the function that will handle + * onended/oninactive events of the stream. + */ +function JitsiTrack(rtc, stream, streamInactiveHandler) +{ + /** + * Array with the HTML elements that are displaying the streams. + * @type {Array} + */ + this.containers = []; + this.rtc = rtc; + this.stream = stream; + this.type = (this.stream.getVideoTracks().length > 0)? + JitsiTrack.VIDEO : JitsiTrack.AUDIO; + if(this.type == "audio") { + this._getTracks = function () { + return this.stream.getAudioTracks(); + }.bind(this); + } else { + this._getTracks = function () { + return this.stream.getVideoTracks(); + }.bind(this); + } + if (RTCBrowserType.isFirefox() && this.stream) { + implementOnEndedHandling(this); + } + + if(stream) + addMediaStreamInactiveHandler(stream, streamInactiveHandler); +} + +/** + * JitsiTrack video type. + * @type {string} + */ +JitsiTrack.VIDEO = "video"; + +/** + * JitsiTrack audio type. + * @type {string} + */ +JitsiTrack.AUDIO = "audio"; + +/** + * Returns the type (audio or video) of this track. + */ +JitsiTrack.prototype.getType = function() { + return this.type; +}; + +/** + * Returns the RTCMediaStream from the browser (?). + */ +JitsiTrack.prototype.getOriginalStream = function() { + return this.stream; +} + +/** + * Mutes the track. + */ +JitsiTrack.prototype.mute = function () { + this._setMute(true); +} + +/** + * Unmutes the stream. + */ +JitsiTrack.prototype.unmute = function () { + this._setMute(false); +} + +/** + * Attaches the MediaStream of this track to an HTML container (?). + * Adds the container to the list of containers that are displaying the track. + * @param container the HTML container + */ +JitsiTrack.prototype.attach = function (container) { + if(this.stream) + require("./RTCUtils").attachMediaStream(container, this.stream); + this.containers.push(container); +} + +/** + * Removes the track from the passed HTML container. + * @param container the HTML container. If null all containers are removed. + */ +JitsiTrack.prototype.detach = function (container) { + for(var i = 0; i < this.containers.length; i++) + { + if(this.containers[i].is(container)) + { + this.containers.splice(i,1); + } + if(!container) + { + this.containers[i].find(">video").remove(); + } + } + if(container) + $(container).find(">video").remove(); + +} + +/** + * Stops sending the media track. And removes it from the HTML. + * NOTE: Works for local tracks only. + */ +JitsiTrack.prototype.stop = function () { +} + +/** + * Returns true if this is a video track and the source of the video is a + * screen capture as opposed to a camera. + */ +JitsiTrack.prototype.isScreenSharing = function(){ + +} + +/** + * Returns id of the track. + * @returns {string} id of the track or null if this is fake track. + */ +JitsiTrack.prototype.getId = function () { + var tracks = this.stream.getTracks(); + if(!tracks || tracks.length === 0) + return null; + return tracks[0].id; +}; + +/** + * Checks whether the MediaStream is avtive/not ended. + * When there is no check for active we don't have information and so + * will return that stream is active (in case of FF). + * @returns {boolean} whether MediaStream is active. + */ +JitsiTrack.prototype.isActive = function () { + if((typeof this.stream.active !== "undefined")) + return this.stream.active; + else + return true; +}; + + +module.exports = JitsiTrack; + +},{"./RTCBrowserType":14,"./RTCUtils":15}],13:[function(require,module,exports){ +/* global APP */ +var EventEmitter = require("events"); +var RTCBrowserType = require("./RTCBrowserType"); +var RTCUtils = require("./RTCUtils.js"); +var JitsiTrack = require("./JitsiTrack"); +var JitsiLocalTrack = require("./JitsiLocalTrack.js"); +var DataChannels = require("./DataChannels"); +var JitsiRemoteTrack = require("./JitsiRemoteTrack.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 desktopsharing = require("../desktopsharing/desktopsharing"); + +function RTC(room, options) { + this.room = room; + this.localStreams = []; + this.remoteStreams = {}; + this.localAudio = null; + this.localVideo = null; + this.eventEmitter = new EventEmitter(); + var self = this; + this.options = options || {}; + desktopsharing.addListener(DesktopSharingEventTypes.NEW_STREAM_CREATED, + function (stream, isUsingScreenStream, callback) { + self.changeLocalVideo(stream, isUsingScreenStream, callback); + }); + room.addPresenceListener("videomuted", function (values, from) { + if(self.remoteStreams[from]) + self.remoteStreams[from][JitsiTrack.VIDEO].setMute(values.value == "true"); + }); + room.addPresenceListener("audiomuted", function (values, from) { + if(self.remoteStreams[from]) + self.remoteStreams[from][JitsiTrack.AUDIO].setMute(values.value == "true"); + }); +} + +/** + * Creates the local MediaStreams. + * @param {Object} [options] optional parameters + * @param {Array} options.devices the devices that will be requested + * @param {string} options.resolution resolution constraints + * @param {bool} options.dontCreateJitsiTrack if true objects with the following structure {stream: the Media Stream, + * type: "audio" or "video", videoType: "camera" or "desktop"} + * will be returned trough the Promise, otherwise JitsiTrack objects will be returned. + * @param {string} options.cameraDeviceId + * @param {string} options.micDeviceId + * @returns {*} Promise object that will receive the new JitsiTracks + */ +RTC.obtainAudioAndVideoPermissions = function (options) { + return RTCUtils.obtainAudioAndVideoPermissions(options); +} + +RTC.prototype.onIncommingCall = function(event) { + if(this.options.config.openSctp) + this.dataChannels = new DataChannels(event.peerconnection, this.eventEmitter); + for(var i = 0; i < this.localStreams.length; i++) + if(this.localStreams[i]) + { + this.room.addStream(this.localStreams[i].getOriginalStream(), function () {}); + } +} + +RTC.prototype.selectedEndpoint = function (id) { + if(this.dataChannels) + this.dataChannels.handleSelectedEndpointEvent(id); +} + +RTC.prototype.pinEndpoint = function (id) { + if(this.dataChannels) + this.dataChannels.handlePinnedEndpointEvent(id); +} + +RTC.prototype.addListener = function (type, listener) { + this.eventEmitter.on(type, listener); +}; + +RTC.prototype.removeListener = function (eventType, listener) { + this.eventEmitter.removeListener(eventType, listener); +}; + +RTC.addListener = function (eventType, listener) { + RTCUtils.addListener(eventType, listener); +} + +RTC.removeListener = function (eventType, listener) { + RTCUtils.eventEmitter.removeListener(RTCEvents.RTC_READY, listener); + RTCUtils.removeListener(eventType, listener) +} + +RTC.isRTCReady = function () { + return RTCUtils.isRTCReady(); +} + +RTC.init = function (options) { + RTCUtils.init(options || {}); +} + +RTC.getDeviceAvailability = function () { + return RTCUtils.getDeviceAvailability(); +} + +RTC.prototype.addLocalStream = function (stream) { + this.localStreams.push(stream); + stream._setRTC(this); + + if (stream.type == "audio") { + this.localAudio = stream; + } else { + this.localVideo = stream; + } +}; + +RTC.prototype.removeLocalStream = function (stream) { + for(var i = 0; i < this.localStreams.length; i++) { + if(this.localStreams[i].getOriginalStream() === stream) { + delete this.localStreams[i]; + return; + } + } +}; + +RTC.prototype.createRemoteStream = function (data, sid, thessrc) { + var remoteStream = new JitsiRemoteTrack(this, data, sid, thessrc, + this.eventEmitter); + if(!data.peerjid) + return; + var jid = data.peerjid; + if(!this.remoteStreams[jid]) { + this.remoteStreams[jid] = {}; + } + this.remoteStreams[jid][remoteStream.type]= remoteStream; + this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, remoteStream); + return remoteStream; +}; + +RTC.getPCConstraints = function () { + return RTCUtils.pc_constraints; +}; + +RTC.getUserMediaWithConstraints = function(um, success_callback, + failure_callback, options) +{ + return RTCUtils.getUserMediaWithConstraints(this, um, success_callback, + failure_callback, options); +}; + +RTC.attachMediaStream = function (elSelector, stream) { + RTCUtils.attachMediaStream(elSelector, stream); +}; + +RTC.getStreamID = function (stream) { + return RTCUtils.getStreamID(stream); +}; + +RTC.getVideoSrc = function (element) { + return RTCUtils.getVideoSrc(element); +}; + +RTC.isDeviceListAvailable = function () { + return RTCUtils.isDeviceListAvailable(); +}; + +/** + * Allows to receive list of available cameras/microphones. + * @param {function} callback would receive array of devices as an argument + */ +RTC.enumerateDevices = function (callback) { + RTCUtils.enumerateDevices(callback); +}; + +RTC.setVideoSrc = function (element, src) { + RTCUtils.setVideoSrc(element, src); +}; + +/** + * A method to handle stopping of the stream. + * One point to handle the differences in various implementations. + * @param mediaStream MediaStream object to stop. + */ +RTC.stopMediaStream = function (mediaStream) { + RTCUtils.stopMediaStream(mediaStream); +}; + +RTC.prototype.getVideoElementName = function () { + return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video'; +}; + +RTC.prototype.dispose = function() { +}; + +RTC.prototype.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; +}; + +RTC.prototype.switchVideoStreams = function (newStream) { + this.localVideo.stream = newStream; + + this.localStreams = []; + + //in firefox we have only one stream object + if (this.localAudio.getOriginalStream() != newStream) + this.localStreams.push(this.localAudio); + this.localStreams.push(this.localVideo); +}; + +RTC.prototype.isVideoMuted = function (jid) { + if (jid === APP.xmpp.myJid()) { + var localVideo = APP.RTC.localVideo; + return (!localVideo || localVideo.isMuted()); + } else { + if (!this.remoteStreams[jid] || + !this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) { + return null; + } + return this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted; + } +}; + +RTC.prototype.setVideoMute = function (mute, callback, options) { + if (!this.localVideo) + return; + + if (mute == this.localVideo.isMuted()) + { + APP.xmpp.sendVideoInfoPresence(mute); + if (callback) + callback(mute); + } + else + { + this.localVideo.setMute(mute); + this.room.setVideoMute( + mute, + callback, + options); + } +}; + +module.exports = RTC; + +},{"../../service/RTC/MediaStreamTypes":76,"../../service/RTC/RTCEvents.js":77,"../../service/RTC/StreamEventTypes.js":79,"../../service/desktopsharing/DesktopSharingEventTypes":81,"../desktopsharing/desktopsharing":18,"./DataChannels":9,"./JitsiLocalTrack.js":10,"./JitsiRemoteTrack.js":11,"./JitsiTrack":12,"./RTCBrowserType":14,"./RTCUtils.js":15,"events":41}],14:[function(require,module,exports){ + +var currentBrowser; + +var browserVersion; + +var isAndroid; + +var RTCBrowserType = { + + RTC_BROWSER_CHROME: "rtc_browser.chrome", + + RTC_BROWSER_OPERA: "rtc_browser.opera", + + RTC_BROWSER_FIREFOX: "rtc_browser.firefox", + + RTC_BROWSER_IEXPLORER: "rtc_browser.iexplorer", + + RTC_BROWSER_SAFARI: "rtc_browser.safari", + + getBrowserType: function () { + return currentBrowser; + }, + + isChrome: function () { + return currentBrowser === RTCBrowserType.RTC_BROWSER_CHROME; + }, + + isOpera: function () { + return currentBrowser === RTCBrowserType.RTC_BROWSER_OPERA; + }, + isFirefox: function () { + return currentBrowser === RTCBrowserType.RTC_BROWSER_FIREFOX; + }, + + isIExplorer: function () { + return currentBrowser === RTCBrowserType.RTC_BROWSER_IEXPLORER; + }, + + isSafari: function () { + return currentBrowser === RTCBrowserType.RTC_BROWSER_SAFARI; + }, + isTemasysPluginUsed: function () { + return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari(); + }, + getFirefoxVersion: function () { + return RTCBrowserType.isFirefox() ? browserVersion : null; + }, + + getChromeVersion: function () { + return RTCBrowserType.isChrome() ? browserVersion : null; + }, + + usesPlanB: function() { + return RTCBrowserType.isChrome() || RTCBrowserType.isOpera() || + RTCBrowserType.isTemasysPluginUsed(); + }, + + usesUnifiedPlan: function() { + return RTCBrowserType.isFirefox(); + }, + + /** + * Whether the browser is running on an android device. + */ + isAndroid: function() { + return isAndroid; + } + + // Add version getters for other browsers when needed +}; + +// detectOpera() must be called before detectChrome() !!! +// otherwise Opera wil be detected as Chrome +function detectChrome() { + if (navigator.webkitGetUserMedia) { + currentBrowser = RTCBrowserType.RTC_BROWSER_CHROME; + 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("This appears to be Chrome, ver: " + ver); + return ver; + } + return null; +} + +function detectOpera() { + var userAgent = navigator.userAgent; + if (userAgent.match(/Opera|OPR/)) { + currentBrowser = RTCBrowserType.RTC_BROWSER_OPERA; + var version = userAgent.match(/(Opera|OPR) ?\/?(\d+)\.?/)[2]; + console.info("This appears to be Opera, ver: " + version); + return version; + } + return null; +} + +function detectFirefox() { + if (navigator.mozGetUserMedia) { + currentBrowser = RTCBrowserType.RTC_BROWSER_FIREFOX; + var version = parseInt( + navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + console.log('This appears to be Firefox, ver: ' + version); + return version; + } + return null; +} + +function detectSafari() { + if ((/^((?!chrome).)*safari/i.test(navigator.userAgent))) { + currentBrowser = RTCBrowserType.RTC_BROWSER_SAFARI; + console.info("This appears to be Safari"); + // FIXME detect Safari version when needed + return 1; + } + return null; +} + +function detectIE() { + var version; + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + version = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (!version && trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + version = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (!version && edge > 0) { + // IE 12 => return version number + version = parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + if (version) { + currentBrowser = RTCBrowserType.RTC_BROWSER_IEXPLORER; + console.info("This appears to be IExplorer, ver: " + version); + } + return version; +} + +function detectBrowser() { + var version; + var detectors = [ + detectOpera, + detectChrome, + detectFirefox, + detectIE, + detectSafari + ]; + // Try all browser detectors + for (var i = 0; i < detectors.length; i++) { + version = detectors[i](); + if (version) + return version; + } + console.error("Failed to detect browser type"); + return undefined; +} + +browserVersion = detectBrowser(); +isAndroid = navigator.userAgent.indexOf('Android') != -1; + +module.exports = RTCBrowserType; +},{}],15:[function(require,module,exports){ +(function (__filename){ +/* global config, require, attachMediaStream, getUserMedia */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var RTCBrowserType = require("./RTCBrowserType"); +var Resolutions = require("../../service/RTC/Resolutions"); +var RTCEvents = require("../../service/RTC/RTCEvents"); +var AdapterJS = require("./adapter.screenshare"); +var SDPUtil = require("../xmpp/SDPUtil"); +var EventEmitter = require("events"); +var JitsiLocalTrack = require("./JitsiLocalTrack"); +var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js"); + +var eventEmitter = new EventEmitter(); + +var devices = { + audio: true, + video: true +} + +var rtcReady = false; + +function DummyMediaStream(id) { + this.id = id; + this.label = id; + this.stop = function() { }; + this.getAudioTracks = function() { return []; }; + this.getVideoTracks = function() { return []; }; +} + +function getPreviousResolution(resolution) { + if(!Resolutions[resolution]) + return null; + var order = Resolutions[resolution].order; + var res = null; + var resName = null; + var tmp, i; + for(i in Resolutions) { + tmp = Resolutions[i]; + if (!res || (res.order < tmp.order && tmp.order < order)) { + resName = i; + res = tmp; + } + } + return resName; +} + +function setResolutionConstraints(constraints, resolution) { + var isAndroid = RTCBrowserType.isAndroid(); + + if (Resolutions[resolution]) { + constraints.video.mandatory.minWidth = Resolutions[resolution].width; + constraints.video.mandatory.minHeight = Resolutions[resolution].height; + } + else if (isAndroid) { + // FIXME can't remember if the purpose of this was to always request + // low resolution on Android ? if yes it should be moved up front + constraints.video.mandatory.minWidth = 320; + constraints.video.mandatory.minHeight = 240; + constraints.video.mandatory.maxFrameRate = 15; + } + + if (constraints.video.mandatory.minWidth) + constraints.video.mandatory.maxWidth = + constraints.video.mandatory.minWidth; + if (constraints.video.mandatory.minHeight) + constraints.video.mandatory.maxHeight = + constraints.video.mandatory.minHeight; +} +/** + * @param {string[]} um required user media types + * + * @param {Object} [options={}] optional parameters + * @param {string} options.resolution + * @param {number} options.bandwidth + * @param {number} options.fps + * @param {string} options.desktopStream + * @param {string} options.cameraDeviceId + * @param {string} options.micDeviceId + * @param {bool} firefox_fake_device + */ +function getConstraints(um, options) { + var constraints = {audio: false, video: false}; + + if (um.indexOf('video') >= 0) { + // same behaviour as true + constraints.video = { mandatory: {}, optional: [] }; + + if (options.cameraDeviceId) { + constraints.video.optional.push({ + sourceId: options.cameraDeviceId + }); + } + + constraints.video.optional.push({ googLeakyBucket: true }); + + setResolutionConstraints(constraints, options.resolution); + } + if (um.indexOf('audio') >= 0) { + if (!RTCBrowserType.isFirefox()) { + // same behaviour as true + constraints.audio = { mandatory: {}, optional: []}; + if (options.micDeviceId) { + constraints.audio.optional.push({ + sourceId: options.micDeviceId + }); + } + // if it is good enough for hangouts... + constraints.audio.optional.push( + {googEchoCancellation: true}, + {googAutoGainControl: true}, + {googNoiseSupression: true}, + {googHighpassFilter: true}, + {googNoisesuppression2: true}, + {googEchoCancellation2: true}, + {googAutoGainControl2: true} + ); + } else { + if (options.micDeviceId) { + constraints.audio = { + mandatory: {}, + optional: [{ + sourceId: options.micDeviceId + }]}; + } else { + constraints.audio = true; + } + } + } + if (um.indexOf('screen') >= 0) { + if (RTCBrowserType.isChrome()) { + constraints.video = { + mandatory: { + chromeMediaSource: "screen", + googLeakyBucket: true, + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3 + }, + optional: [] + }; + } else if (RTCBrowserType.isTemasysPluginUsed()) { + constraints.video = { + optional: [ + { + sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey + } + ] + }; + } else if (RTCBrowserType.isFirefox()) { + constraints.video = { + mozMediaSource: "window", + mediaSource: "window" + }; + + } else { + logger.error( + "'screen' WebRTC media source is supported only in Chrome" + + " and with Temasys plugin"); + } + } + if (um.indexOf('desktop') >= 0) { + constraints.video = { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: options.desktopStream, + googLeakyBucket: true, + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3 + }, + optional: [] + }; + } + + if (options.bandwidth) { + if (!constraints.video) { + //same behaviour as true + constraints.video = {mandatory: {}, optional: []}; + } + constraints.video.optional.push({bandwidth: options.bandwidth}); + } + if (options.fps) { + // for some cameras it might be necessary to request 30fps + // so they choose 30fps mjpg over 10fps yuy2 + if (!constraints.video) { + // same behaviour as true; + constraints.video = {mandatory: {}, optional: []}; + } + constraints.video.mandatory.minFrameRate = options.fps; + } + + // we turn audio for both audio and video tracks, the fake audio & video seems to work + // only when enabled in one getUserMedia call, we cannot get fake audio separate by fake video + // this later can be a problem with some of the tests + if(RTCBrowserType.isFirefox() && options.firefox_fake_device) + { + constraints.audio = true; + constraints.fake = true; + } + + return constraints; +} + +function setAvailableDevices(um, available) { + if (um.indexOf("video") != -1) { + devices.video = available; + } + if (um.indexOf("audio") != -1) { + devices.audio = available; + } + + eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, devices); +} + +// In case of IE we continue from 'onReady' callback +// passed to RTCUtils constructor. It will be invoked by Temasys plugin +// once it is initialized. +function onReady () { + rtcReady = true; + eventEmitter.emit(RTCEvents.RTC_READY, true); +}; + +/** + * Apply function with arguments if function exists. + * Do nothing if function not provided. + * @param {function} [fn] function to apply + * @param {Array} [args=[]] arguments for function + */ +function maybeApply(fn, args) { + if (fn) { + fn.apply(null, args || []); + } +} + +var getUserMediaStatus = { + initialized: false, + callbacks: [] +}; + +/** + * Wrap `getUserMedia` to allow others to know if it was executed at least + * once or not. Wrapper function uses `getUserMediaStatus` object. + * @param {Function} getUserMedia native function + * @returns {Function} wrapped function + */ +function wrapGetUserMedia(getUserMedia) { + return function (constraints, successCallback, errorCallback) { + getUserMedia(constraints, function (stream) { + maybeApply(successCallback, [stream]); + if (!getUserMediaStatus.initialized) { + getUserMediaStatus.initialized = true; + getUserMediaStatus.callbacks.forEach(function (callback) { + callback(); + }); + getUserMediaStatus.callbacks.length = 0; + } + }, function (error) { + maybeApply(errorCallback, [error]); + }); + }; +} + +/** + * Create stub device which equals to auto selected device. + * @param {string} kind if that should be `audio` or `video` device + * @returns {Object} stub device description in `enumerateDevices` format + */ +function createAutoDeviceInfo(kind) { + return { + facing: null, + label: 'Auto', + kind: kind, + deviceId: '', + groupId: null + }; +} + + +/** + * Execute function after getUserMedia was executed at least once. + * @param {Function} callback function to execute after getUserMedia + */ +function afterUserMediaInitialized(callback) { + if (getUserMediaStatus.initialized) { + callback(); + } else { + getUserMediaStatus.callbacks.push(callback); + } +} + +/** + * Wrapper function which makes enumerateDevices to wait + * until someone executes getUserMedia first time. + * @param {Function} enumerateDevices native function + * @returns {Funtion} wrapped function + */ +function wrapEnumerateDevices(enumerateDevices) { + return function (callback) { + // enumerate devices only after initial getUserMedia + afterUserMediaInitialized(function () { + + enumerateDevices().then(function (devices) { + //add auto devices + devices.unshift( + createAutoDeviceInfo('audioinput'), + createAutoDeviceInfo('videoinput') + ); + + callback(devices); + }, function (err) { + console.error('cannot enumerate devices: ', err); + + // return only auto devices + callback([createAutoDeviceInfo('audioInput'), + createAutoDeviceInfo('videoinput')]); + }); + }); + }; +} + +/** + * Use old MediaStreamTrack to get devices list and + * convert it to enumerateDevices format. + * @param {Function} callback function to call when received devices list. + */ +function enumerateDevicesThroughMediaStreamTrack (callback) { + MediaStreamTrack.getSources(function (sources) { + var devices = sources.map(function (source) { + var kind = (source.kind || '').toLowerCase(); + return { + facing: source.facing || null, + label: source.label, + kind: kind ? kind + 'input': null, + deviceId: source.id, + groupId: source.groupId || null + }; + }); + + //add auto devices + devices.unshift( + createAutoDeviceInfo('audioinput'), + createAutoDeviceInfo('videoinput') + ); + callback(devices); + }); +} + +//Options parameter is to pass config options. Currently uses only "useIPv6". +var RTCUtils = { + init: function (options) { + var self = this; + if (RTCBrowserType.isFirefox()) { + var FFversion = RTCBrowserType.getFirefoxVersion(); + if (FFversion >= 40) { + this.peerconnection = mozRTCPeerConnection; + this.getUserMedia = wrapGetUserMedia(navigator.mozGetUserMedia.bind(navigator)); + this.enumerateDevices = wrapEnumerateDevices( + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices) + ); + this.pc_constraints = {}; + this.attachMediaStream = function (element, stream) { + // srcObject is being standardized and FF will eventually + // support that unprefixed. FF also supports the + // "element.src = URL.createObjectURL(...)" combo, but that + // will be deprecated in favour of srcObject. + // + // https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg + // https://github.com/webrtc/samples/issues/302 + if (!element[0]) + return; + element[0].mozSrcObject = stream; + element[0].play(); + }; + this.getStreamID = function (stream) { + var id = stream.id; + if (!id) { + var tracks = stream.getVideoTracks(); + if (!tracks || tracks.length === 0) { + tracks = stream.getAudioTracks(); + } + id = tracks[0].id; + } + return SDPUtil.filter_special_chars(id); + }; + this.getVideoSrc = function (element) { + if (!element) + return null; + return element.mozSrcObject; + }; + this.setVideoSrc = function (element, src) { + if (element) + element.mozSrcObject = src; + }; + RTCSessionDescription = mozRTCSessionDescription; + RTCIceCandidate = mozRTCIceCandidate; + } else { + logger.error( + "Firefox version too old: " + FFversion + ". Required >= 40."); + window.location.href = 'unsupported_browser.html'; + return; + } + + } else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) { + this.peerconnection = webkitRTCPeerConnection; + var getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + if (navigator.mediaDevices) { + this.getUserMedia = wrapGetUserMedia(getUserMedia); + this.enumerateDevices = wrapEnumerateDevices( + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices) + ); + } else { + this.getUserMedia = getUserMedia; + this.enumerateDevices = enumerateDevicesThroughMediaStreamTrack; + } + 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 SDPUtil.filter_special_chars(stream.id); + }; + this.getVideoSrc = function (element) { + if (!element) + return null; + return element.getAttribute("src"); + }; + this.setVideoSrc = function (element, src) { + if (element) + element.setAttribute("src", src); + }; + // DTLS should now be enabled by default but.. + this.pc_constraints = {'optional': [ + {'DtlsSrtpKeyAgreement': 'true'} + ]}; + if (options.useIPv6) { + // https://code.google.com/p/webrtc/issues/detail?id=2828 + this.pc_constraints.optional.push({googIPv6: true}); + } + if (RTCBrowserType.isAndroid()) { + this.pc_constraints = {}; // disable DTLS on Android + } + if (!webkitMediaStream.prototype.getVideoTracks) { + webkitMediaStream.prototype.getVideoTracks = function () { + return this.videoTracks; + }; + } + if (!webkitMediaStream.prototype.getAudioTracks) { + webkitMediaStream.prototype.getAudioTracks = function () { + return this.audioTracks; + }; + } + } + // Detect IE/Safari + else if (RTCBrowserType.isTemasysPluginUsed()) { + + //AdapterJS.WebRTCPlugin.setLogLevel( + // AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE); + + AdapterJS.webRTCReady(function (isPlugin) { + + self.peerconnection = RTCPeerConnection; + self.getUserMedia = window.getUserMedia; + self.enumerateDevices = enumerateDevicesThroughMediaStreamTrack; + self.attachMediaStream = function (elSel, stream) { + + if (stream.id === "dummyAudio" || stream.id === "dummyVideo") { + return; + } + + attachMediaStream(elSel[0], stream); + }; + self.getStreamID = function (stream) { + var id = SDPUtil.filter_special_chars(stream.label); + return id; + }; + self.getVideoSrc = function (element) { + if (!element) { + logger.warn("Attempt to get video SRC of null element"); + return null; + } + var children = element.children; + for (var i = 0; i !== children.length; ++i) { + if (children[i].name === 'streamId') { + return children[i].value; + } + } + //logger.info(element.id + " SRC: " + src); + return null; + }; + self.setVideoSrc = function (element, src) { + //logger.info("Set video src: ", element, src); + if (!src) { + logger.warn("Not attaching video stream, 'src' is null"); + return; + } + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + var stream = AdapterJS.WebRTCPlugin.plugin + .getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src); + attachMediaStream(element, stream); + }; + + onReady(isPlugin); + }); + } else { + try { + logger.error('Browser does not appear to be WebRTC-capable'); + } catch (e) { + } + return; + } + + // Call onReady() if Temasys plugin is not used + if (!RTCBrowserType.isTemasysPluginUsed()) { + onReady(); + } + + }, + /** + * @param {string[]} um required user media types + * @param {function} success_callback + * @param {Function} failure_callback + * @param {Object} [options] optional parameters + * @param {string} options.resolution + * @param {number} options.bandwidth + * @param {number} options.fps + * @param {string} options.desktopStream + * @param {string} options.cameraDeviceId + * @param {string} options.micDeviceId + **/ + getUserMediaWithConstraints: function ( um, success_callback, failure_callback, options) { + options = options || {}; + resolution = options.resolution; + var constraints = getConstraints( + um, options); + + logger.info("Get media constraints", constraints); + + try { + this.getUserMedia(constraints, + function (stream) { + logger.log('onUserMediaSuccess'); + setAvailableDevices(um, true); + success_callback(stream); + }, + function (error) { + setAvailableDevices(um, false); + logger.warn('Failed to get access to local media. Error ', + error, constraints); + if (failure_callback) { + failure_callback(error, resolution); + } + }); + } catch (e) { + logger.error('GUM failed: ', e); + if (failure_callback) { + failure_callback(e); + } + } + }, + + /** + * Creates the local MediaStreams. + * @param {Object} [options] optional parameters + * @param {Array} options.devices the devices that will be requested + * @param {string} options.resolution resolution constraints + * @param {bool} options.dontCreateJitsiTrack if true objects with the following structure {stream: the Media Stream, + * type: "audio" or "video", videoType: "camera" or "desktop"} + * will be returned trough the Promise, otherwise JitsiTrack objects will be returned. + * @param {string} options.cameraDeviceId + * @param {string} options.micDeviceId + * @returns {*} Promise object that will receive the new JitsiTracks + */ + obtainAudioAndVideoPermissions: function (options) { + var self = this; + + options = options || {}; + return new Promise(function (resolve, reject) { + var successCallback = function (stream) { + var streams = self.successCallback(stream, options.resolution); + resolve(options.dontCreateJitsiTracks? + streams: self.createLocalTracks(streams)); + }; + + options.devices = options.devices || ['audio', 'video']; + + if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) { + + // With FF/IE we can't split the stream into audio and video because FF + // doesn't support media stream constructors. So, we need to get the + // audio stream separately from the video stream using two distinct GUM + // calls. Not very user friendly :-( but we don't have many other + // options neither. + // + // Note that we pack those 2 streams in a single object and pass it to + // the successCallback method. + var obtainVideo = function (audioStream) { + self.getUserMediaWithConstraints( + ['video'], + function (videoStream) { + return successCallback({ + audioStream: audioStream, + videoStream: videoStream + }); + }, + function (error, resolution) { + logger.error( + 'failed to obtain video stream - stop', error); + self.errorCallback(error, resolve, options); + }, + {resolution: options.resolution || '360', + cameraDeviceId: options.cameraDeviceId}); + }; + var obtainAudio = function () { + self.getUserMediaWithConstraints( + ['audio'], + function (audioStream) { + (options.devices.indexOf('video') === -1) || + obtainVideo(audioStream); + }, + function (error) { + logger.error( + 'failed to obtain audio stream - stop', error); + self.errorCallback(error, resolve, options); + },{micDeviceId: options.micDeviceId}); + }; + if((options.devices.indexOf('audio') === -1)) + obtainVideo(null) + else + obtainAudio(); + } else { + this.getUserMediaWithConstraints( + options.devices, + function (stream) { + successCallback(stream); + }, + function (error, resolution) { + self.errorCallback(error, resolve, options); + }, + {resolution: options.resolution || '360', + cameraDeviceId: options.cameraDeviceId, + micDeviceId: options.micDeviceId}); + } + }.bind(this)); + }, + + /** + * Successful callback called from GUM. + * @param stream the new MediaStream + * @param resolution the resolution of the video stream. + * @returns {*} + */ + successCallback: function (stream, resolution) { + // If this is FF or IE, the stream parameter is *not* a MediaStream object, + // it's an object with two properties: audioStream, videoStream. + if (stream && stream.getAudioTracks && stream.getVideoTracks) + logger.log('got', stream, stream.getAudioTracks().length, + stream.getVideoTracks().length); + return this.handleLocalStream(stream, resolution); + }, + + /** + * Error callback called from GUM. Retries the GUM call with different resolutions. + * @param error the error + * @param resolve the resolve funtion that will be called on success. + * @param {Object} options with the following properties: + * @param resolution the last resolution used for GUM. + * @param dontCreateJitsiTracks if true objects with the following structure {stream: the Media Stream, + * type: "audio" or "video", videoType: "camera" or "desktop"} + * will be returned trough the Promise, otherwise JitsiTrack objects will be returned. + */ + errorCallback: function (error, resolve, options) { + var self = this; + options = options || {}; + logger.error('failed to obtain audio/video stream - trying audio only', error); + var resolution = getPreviousResolution(options.resolution); + if (typeof error == "object" && error.constraintName && error.name + && (error.name == "ConstraintNotSatisfiedError" || + error.name == "OverconstrainedError") && + (error.constraintName == "minWidth" || error.constraintName == "maxWidth" || + error.constraintName == "minHeight" || error.constraintName == "maxHeight") + && resolution) { + self.getUserMediaWithConstraints(['audio', 'video'], + function (stream) { + var streams = self.successCallback(stream, resolution); + resolve(options.dontCreateJitsiTracks? streams: self.createLocalTracks(streams)); + }, function (error, resolution) { + return self.errorCallback(error, resolve, + {resolution: resolution, + dontCreateJitsiTracks: options.dontCreateJitsiTracks}); + }, + {resolution: options.resolution}); + } + else { + self.getUserMediaWithConstraints( + ['audio'], + function (stream) { + var streams = self.successCallback(stream, resolution); + resolve(options.dontCreateJitsiTracks? streams: self.createLocalTracks(streams)); + }, + function (error) { + logger.error('failed to obtain audio/video stream - stop', + error); + var streams = self.successCallback(null); + resolve(options.dontCreateJitsiTracks? streams: self.createLocalTracks(streams)); + } + ); + } + }, + + /** + * Handles the newly created Media Streams. + * @param stream the new Media Streams + * @param resolution the resolution of the video stream. + * @returns {*[]} Promise object with the new Media Streams. + */ + handleLocalStream: function (stream, resolution) { + var audioStream, videoStream; + // If this is FF, the stream parameter is *not* a MediaStream object, it's + // an object with two properties: audioStream, videoStream. + if (window.webkitMediaStream) { + audioStream = new webkitMediaStream(); + videoStream = new webkitMediaStream(); + if (stream) { + var audioTracks = stream.getAudioTracks(); + + for (var i = 0; i < audioTracks.length; i++) { + audioStream.addTrack(audioTracks[i]); + } + + var videoTracks = stream.getVideoTracks(); + + for (i = 0; i < videoTracks.length; i++) { + videoStream.addTrack(videoTracks[i]); + } + } + + } + else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) { // Firefox and Temasys plugin + if (stream && stream.audioStream) + audioStream = stream.audioStream; + else + audioStream = new DummyMediaStream("dummyAudio"); + + if (stream && stream.videoStream) + videoStream = stream.videoStream; + else + videoStream = new DummyMediaStream("dummyVideo"); + } + + return [ + {stream: audioStream, type: "audio", videoType: null}, + {stream: videoStream, type: "video", videoType: "camera", + resolution: resolution} + ]; + }, + + createStream: function (stream, isVideo) { + var newStream = null; + if (window.webkitMediaStream) { + newStream = new webkitMediaStream(); + if (newStream) { + var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks()); + + for (var i = 0; i < tracks.length; i++) { + newStream.addTrack(tracks[i]); + } + } + + } else { + // FIXME: this is duplicated with 'handleLocalStream' !!! + if (stream) { + newStream = stream; + } else { + newStream = + new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio"); + } + } + + return newStream; + }, + addListener: function (eventType, listener) { + eventEmitter.on(eventType, listener); + }, + removeListener: function (eventType, listener) { + eventEmitter.removeListener(eventType, listener); + }, + getDeviceAvailability: function () { + return devices; + }, + isRTCReady: function () { + return rtcReady; + }, + createLocalTracks: function (streams) { + var newStreams = [] + for (var i = 0; i < streams.length; i++) { + var localStream = new JitsiLocalTrack(null, streams[i].stream, + eventEmitter, streams[i].videoType, streams[i].resolution); + newStreams.push(localStream); + if (streams[i].isMuted === true) + localStream.setMute(true); + + var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED; + + eventEmitter.emit(eventType, localStream); + } + return newStreams; + }, + + /** + * Checks if its possible to enumerate available cameras/micropones. + * @returns {boolean} true if available, false otherwise. + */ + isDeviceListAvailable: function () { + var isEnumerateDevicesAvailable = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices; + if (isEnumerateDevicesAvailable) { + return true; + } + return (MediaStreamTrack && MediaStreamTrack.getSources)? true : false; + }, + /** + * A method to handle stopping of the stream. + * One point to handle the differences in various implementations. + * @param mediaStream MediaStream object to stop. + */ + stopMediaStream: function (mediaStream) { + mediaStream.getTracks().forEach(function (track) { + // stop() not supported with IE + if (track.stop) { + track.stop(); + } + }); + + // leave stop for implementation still using it + if (mediaStream.stop) { + mediaStream.stop(); + } + } + +}; + +module.exports = RTCUtils; + +}).call(this,"/modules/RTC/RTCUtils.js") +},{"../../service/RTC/RTCEvents":77,"../../service/RTC/Resolutions":78,"../../service/RTC/StreamEventTypes.js":79,"../xmpp/SDPUtil":30,"./JitsiLocalTrack":10,"./RTCBrowserType":14,"./adapter.screenshare":16,"events":41,"jitsi-meet-logger":45}],16:[function(require,module,exports){ +(function (__filename){ +/*! adapterjs - v0.12.0 - 2015-09-04 */ +var console = require("jitsi-meet-logger").getLogger(__filename); + +// Adapter's interface. +var AdapterJS = AdapterJS || {}; + +// Browserify compatibility +if(typeof exports !== 'undefined') { + module.exports = AdapterJS; +} + +AdapterJS.options = AdapterJS.options || {}; + +// uncomment to get virtual webcams +// AdapterJS.options.getAllCams = true; + +// uncomment to prevent the install prompt when the plugin in not yet installed +// AdapterJS.options.hidePluginInstallPrompt = true; + +// AdapterJS version +AdapterJS.VERSION = '0.12.0'; + +// This function will be called when the WebRTC API is ready to be used +// Whether it is the native implementation (Chrome, Firefox, Opera) or +// the plugin +// You may Override this function to synchronise the start of your application +// with the WebRTC API being ready. +// If you decide not to override use this synchronisation, it may result in +// an extensive CPU usage on the plugin start (once per tab loaded) +// Params: +// - isUsingPlugin: true is the WebRTC plugin is being used, false otherwise +// +AdapterJS.onwebrtcready = AdapterJS.onwebrtcready || function(isUsingPlugin) { + // The WebRTC API is ready. + // Override me and do whatever you want here +}; + +// Sets a callback function to be called when the WebRTC interface is ready. +// The first argument is the function to callback.\ +// Throws an error if the first argument is not a function +AdapterJS.webRTCReady = function (callback) { + if (typeof callback !== 'function') { + throw new Error('Callback provided is not a function'); + } + + if (true === AdapterJS.onwebrtcreadyDone) { + // All WebRTC interfaces are ready, just call the callback + callback(null !== AdapterJS.WebRTCPlugin.plugin); + } else { + // will be triggered automatically when your browser/plugin is ready. + AdapterJS.onwebrtcready = callback; + } +}; + +// Plugin namespace +AdapterJS.WebRTCPlugin = AdapterJS.WebRTCPlugin || {}; + +// The object to store plugin information +AdapterJS.WebRTCPlugin.pluginInfo = { + prefix : 'Tem', + plugName : 'TemWebRTCPlugin', + pluginId : 'plugin0', + type : 'application/x-temwebrtcplugin', + onload : '__TemWebRTCReady0', + portalLink : 'http://skylink.io/plugin/', + downloadLink : null, //set below + companyName: 'Temasys' +}; +if(!!navigator.platform.match(/^Mac/i)) { + AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1n77hco'; +} +else if(!!navigator.platform.match(/^Win/i)) { + AdapterJS.WebRTCPlugin.pluginInfo.downloadLink = 'http://bit.ly/1kkS4FN'; +} + +AdapterJS.WebRTCPlugin.TAGS = { + NONE : 'none', + AUDIO : 'audio', + VIDEO : 'video' +}; + +// Unique identifier of each opened page +AdapterJS.WebRTCPlugin.pageId = Math.random().toString(36).slice(2); + +// Use this whenever you want to call the plugin. +AdapterJS.WebRTCPlugin.plugin = null; + +// Set log level for the plugin once it is ready. +// The different values are +// This is an asynchronous function that will run when the plugin is ready +AdapterJS.WebRTCPlugin.setLogLevel = null; + +// Defines webrtc's JS interface according to the plugin's implementation. +// Define plugin Browsers as WebRTC Interface. +AdapterJS.WebRTCPlugin.defineWebRTCInterface = null; + +// This function detects whether or not a plugin is installed. +// Checks if Not IE (firefox, for example), else if it's IE, +// we're running IE and do something. If not it is not supported. +AdapterJS.WebRTCPlugin.isPluginInstalled = null; + + // Lets adapter.js wait until the the document is ready before injecting the plugin +AdapterJS.WebRTCPlugin.pluginInjectionInterval = null; + +// Inject the HTML DOM object element into the page. +AdapterJS.WebRTCPlugin.injectPlugin = null; + +// States of readiness that the plugin goes through when +// being injected and stated +AdapterJS.WebRTCPlugin.PLUGIN_STATES = { + NONE : 0, // no plugin use + INITIALIZING : 1, // Detected need for plugin + INJECTING : 2, // Injecting plugin + INJECTED: 3, // Plugin element injected but not usable yet + READY: 4 // Plugin ready to be used +}; + +// Current state of the plugin. You cannot use the plugin before this is +// equal to AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY +AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.NONE; + +// True is AdapterJS.onwebrtcready was already called, false otherwise +// Used to make sure AdapterJS.onwebrtcready is only called once +AdapterJS.onwebrtcreadyDone = false; + +// Log levels for the plugin. +// To be set by calling AdapterJS.WebRTCPlugin.setLogLevel +/* +Log outputs are prefixed in some cases. + INFO: Information reported by the plugin. + ERROR: Errors originating from within the plugin. + WEBRTC: Error originating from within the libWebRTC library +*/ +// From the least verbose to the most verbose +AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS = { + NONE : 'NONE', + ERROR : 'ERROR', + WARNING : 'WARNING', + INFO: 'INFO', + VERBOSE: 'VERBOSE', + SENSITIVE: 'SENSITIVE' +}; + +// Does a waiting check before proceeding to load the plugin. +AdapterJS.WebRTCPlugin.WaitForPluginReady = null; + +// This methid will use an interval to wait for the plugin to be ready. +AdapterJS.WebRTCPlugin.callWhenPluginReady = null; + +// !!!! WARNING: DO NOT OVERRIDE THIS FUNCTION. !!! +// This function will be called when plugin is ready. It sends necessary +// details to the plugin. +// The function will wait for the document to be ready and the set the +// plugin state to AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY, +// indicating that it can start being requested. +// This function is not in the IE/Safari condition brackets so that +// TemPluginLoaded function might be called on Chrome/Firefox. +// This function is the only private function that is not encapsulated to +// allow the plugin method to be called. +__TemWebRTCReady0 = function () { + if (document.readyState === 'complete') { + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY; + + AdapterJS.maybeThroughWebRTCReady(); + } else { + AdapterJS.WebRTCPlugin.documentReadyInterval = setInterval(function () { + if (document.readyState === 'complete') { + // TODO: update comments, we wait for the document to be ready + clearInterval(AdapterJS.WebRTCPlugin.documentReadyInterval); + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY; + + AdapterJS.maybeThroughWebRTCReady(); + } + }, 100); + } +}; + +AdapterJS.maybeThroughWebRTCReady = function() { + if (!AdapterJS.onwebrtcreadyDone) { + AdapterJS.onwebrtcreadyDone = true; + + if (typeof(AdapterJS.onwebrtcready) === 'function') { + AdapterJS.onwebrtcready(AdapterJS.WebRTCPlugin.plugin !== null); + } + } +}; + +// Text namespace +AdapterJS.TEXT = { + PLUGIN: { + REQUIRE_INSTALLATION: 'This website requires you to install a WebRTC-enabling plugin ' + + 'to work on this browser.', + NOT_SUPPORTED: 'Your browser does not support WebRTC.', + BUTTON: 'Install Now' + }, + REFRESH: { + REQUIRE_REFRESH: 'Please refresh page', + BUTTON: 'Refresh Page' + } +}; + +// The result of ice connection states. +// - starting: Ice connection is starting. +// - checking: Ice connection is checking. +// - connected Ice connection is connected. +// - completed Ice connection is connected. +// - done Ice connection has been completed. +// - disconnected Ice connection has been disconnected. +// - failed Ice connection has failed. +// - closed Ice connection is closed. +AdapterJS._iceConnectionStates = { + starting : 'starting', + checking : 'checking', + connected : 'connected', + completed : 'connected', + done : 'completed', + disconnected : 'disconnected', + failed : 'failed', + closed : 'closed' +}; + +//The IceConnection states that has been fired for each peer. +AdapterJS._iceConnectionFiredStates = []; + + +// Check if WebRTC Interface is defined. +AdapterJS.isDefined = null; + +// This function helps to retrieve the webrtc detected browser information. +// This sets: +// - webrtcDetectedBrowser: The browser agent name. +// - webrtcDetectedVersion: The browser version. +// - webrtcDetectedType: The types of webRTC support. +// - 'moz': Mozilla implementation of webRTC. +// - 'webkit': WebKit implementation of webRTC. +// - 'plugin': Using the plugin implementation. +AdapterJS.parseWebrtcDetectedBrowser = function () { + var hasMatch, checkMatch = navigator.userAgent.match( + /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; + if (/trident/i.test(checkMatch[1])) { + hasMatch = /\brv[ :]+(\d+)/g.exec(navigator.userAgent) || []; + webrtcDetectedBrowser = 'IE'; + webrtcDetectedVersion = parseInt(hasMatch[1] || '0', 10); + } else if (checkMatch[1] === 'Chrome') { + hasMatch = navigator.userAgent.match(/\bOPR\/(\d+)/); + if (hasMatch !== null) { + webrtcDetectedBrowser = 'opera'; + webrtcDetectedVersion = parseInt(hasMatch[1], 10); + } + } + if (navigator.userAgent.indexOf('Safari')) { + if (typeof InstallTrigger !== 'undefined') { + webrtcDetectedBrowser = 'firefox'; + } else if (/*@cc_on!@*/ false || !!document.documentMode) { + webrtcDetectedBrowser = 'IE'; + } else if ( + Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0) { + webrtcDetectedBrowser = 'safari'; + } else if (!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) { + webrtcDetectedBrowser = 'opera'; + } else if (!!window.chrome) { + webrtcDetectedBrowser = 'chrome'; + } + } + if (!webrtcDetectedBrowser) { + webrtcDetectedVersion = checkMatch[1]; + } + if (!webrtcDetectedVersion) { + try { + checkMatch = (checkMatch[2]) ? [checkMatch[1], checkMatch[2]] : + [navigator.appName, navigator.appVersion, '-?']; + if ((hasMatch = navigator.userAgent.match(/version\/(\d+)/i)) !== null) { + checkMatch.splice(1, 1, hasMatch[1]); + } + webrtcDetectedVersion = parseInt(checkMatch[1], 10); + } catch (error) { } + } +}; + +// To fix configuration as some browsers does not support +// the 'urls' attribute. +AdapterJS.maybeFixConfiguration = function (pcConfig) { + if (pcConfig === null) { + return; + } + for (var i = 0; i < pcConfig.iceServers.length; i++) { + if (pcConfig.iceServers[i].hasOwnProperty('urls')) { + pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls; + delete pcConfig.iceServers[i].urls; + } + } +}; + +AdapterJS.addEvent = function(elem, evnt, func) { + if (elem.addEventListener) { // W3C DOM + elem.addEventListener(evnt, func, false); + } else if (elem.attachEvent) {// OLD IE DOM + elem.attachEvent('on'+evnt, func); + } else { // No much to do + elem[evnt] = func; + } +}; + +AdapterJS.renderNotificationBar = function (text, buttonText, buttonLink, openNewTab, displayRefreshBar) { + // only inject once the page is ready + if (document.readyState !== 'complete') { + return; + } + + var w = window; + var i = document.createElement('iframe'); + i.style.position = 'fixed'; + i.style.top = '-41px'; + i.style.left = 0; + i.style.right = 0; + i.style.width = '100%'; + i.style.height = '40px'; + i.style.backgroundColor = '#ffffe1'; + i.style.border = 'none'; + i.style.borderBottom = '1px solid #888888'; + i.style.zIndex = '9999999'; + if(typeof i.style.webkitTransition === 'string') { + i.style.webkitTransition = 'all .5s ease-out'; + } else if(typeof i.style.transition === 'string') { + i.style.transition = 'all .5s ease-out'; + } + document.body.appendChild(i); + c = (i.contentWindow) ? i.contentWindow : + (i.contentDocument.document) ? i.contentDocument.document : i.contentDocument; + c.document.open(); + c.document.write('' + text + ''); + if(buttonText && buttonLink) { + c.document.write(''); + c.document.close(); + + // On click on okay + AdapterJS.addEvent(c.document.getElementById('okay'), 'click', function(e) { + if (!!displayRefreshBar) { + AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION ? + AdapterJS.TEXT.EXTENSION.REQUIRE_REFRESH : AdapterJS.TEXT.REFRESH.REQUIRE_REFRESH, + AdapterJS.TEXT.REFRESH.BUTTON, 'javascript:location.reload()'); + } + window.open(buttonLink, !!openNewTab ? '_blank' : '_top'); + + e.preventDefault(); + try { + event.cancelBubble = true; + } catch(error) { } + + var pluginInstallInterval = setInterval(function(){ + if(! isIE) { + navigator.plugins.refresh(false); + } + AdapterJS.WebRTCPlugin.isPluginInstalled( + AdapterJS.WebRTCPlugin.pluginInfo.prefix, + AdapterJS.WebRTCPlugin.pluginInfo.plugName, + function() { // plugin now installed + clearInterval(pluginInstallInterval); + AdapterJS.WebRTCPlugin.defineWebRTCInterface(); + }, + function() { + // still no plugin detected, nothing to do + }); + } , 500); + }); + + // On click on Cancel + AdapterJS.addEvent(c.document.getElementById('cancel'), 'click', function(e) { + w.document.body.removeChild(i); + }); + } else { + c.document.close(); + } + setTimeout(function() { + if(typeof i.style.webkitTransform === 'string') { + i.style.webkitTransform = 'translateY(40px)'; + } else if(typeof i.style.transform === 'string') { + i.style.transform = 'translateY(40px)'; + } else { + i.style.top = '0px'; + } + }, 300); +}; + +// ----------------------------------------------------------- +// Detected webrtc implementation. Types are: +// - 'moz': Mozilla implementation of webRTC. +// - 'webkit': WebKit implementation of webRTC. +// - 'plugin': Using the plugin implementation. +webrtcDetectedType = null; + +// Detected webrtc datachannel support. Types are: +// - 'SCTP': SCTP datachannel support. +// - 'RTP': RTP datachannel support. +webrtcDetectedDCSupport = null; + +// Set the settings for creating DataChannels, MediaStream for +// Cross-browser compability. +// - This is only for SCTP based support browsers. +// the 'urls' attribute. +checkMediaDataChannelSettings = + function (peerBrowserAgent, peerBrowserVersion, callback, constraints) { + if (typeof callback !== 'function') { + return; + } + var beOfferer = true; + var isLocalFirefox = webrtcDetectedBrowser === 'firefox'; + // Nightly version does not require MozDontOfferDataChannel for interop + var isLocalFirefoxInterop = webrtcDetectedType === 'moz' && webrtcDetectedVersion > 30; + var isPeerFirefox = peerBrowserAgent === 'firefox'; + var isPeerFirefoxInterop = peerBrowserAgent === 'firefox' && + ((peerBrowserVersion) ? (peerBrowserVersion > 30) : false); + + // Resends an updated version of constraints for MozDataChannel to work + // If other userAgent is firefox and user is firefox, remove MozDataChannel + if ((isLocalFirefox && isPeerFirefox) || (isLocalFirefoxInterop)) { + try { + delete constraints.mandatory.MozDontOfferDataChannel; + } catch (error) { + console.error('Failed deleting MozDontOfferDataChannel'); + console.error(error); + } + } else if ((isLocalFirefox && !isPeerFirefox)) { + constraints.mandatory.MozDontOfferDataChannel = true; + } + if (!isLocalFirefox) { + // temporary measure to remove Moz* constraints in non Firefox browsers + for (var prop in constraints.mandatory) { + if (constraints.mandatory.hasOwnProperty(prop)) { + if (prop.indexOf('Moz') !== -1) { + delete constraints.mandatory[prop]; + } + } + } + } + // Firefox (not interopable) cannot offer DataChannel as it will cause problems to the + // interopability of the media stream + if (isLocalFirefox && !isPeerFirefox && !isLocalFirefoxInterop) { + beOfferer = false; + } + callback(beOfferer, constraints); +}; + +// Handles the differences for all browsers ice connection state output. +// - Tested outcomes are: +// - Chrome (offerer) : 'checking' > 'completed' > 'completed' +// - Chrome (answerer) : 'checking' > 'connected' +// - Firefox (offerer) : 'checking' > 'connected' +// - Firefox (answerer): 'checking' > 'connected' +checkIceConnectionState = function (peerId, iceConnectionState, callback) { + if (typeof callback !== 'function') { + console.warn('No callback specified in checkIceConnectionState. Aborted.'); + return; + } + peerId = (peerId) ? peerId : 'peer'; + + if (!AdapterJS._iceConnectionFiredStates[peerId] || + iceConnectionState === AdapterJS._iceConnectionStates.disconnected || + iceConnectionState === AdapterJS._iceConnectionStates.failed || + iceConnectionState === AdapterJS._iceConnectionStates.closed) { + AdapterJS._iceConnectionFiredStates[peerId] = []; + } + iceConnectionState = AdapterJS._iceConnectionStates[iceConnectionState]; + if (AdapterJS._iceConnectionFiredStates[peerId].indexOf(iceConnectionState) < 0) { + AdapterJS._iceConnectionFiredStates[peerId].push(iceConnectionState); + if (iceConnectionState === AdapterJS._iceConnectionStates.connected) { + setTimeout(function () { + AdapterJS._iceConnectionFiredStates[peerId] + .push(AdapterJS._iceConnectionStates.done); + callback(AdapterJS._iceConnectionStates.done); + }, 1000); + } + callback(iceConnectionState); + } + return; +}; + +// Firefox: +// - Creates iceServer from the url for Firefox. +// - Create iceServer with stun url. +// - Create iceServer with turn url. +// - Ignore the transport parameter from TURN url for FF version <=27. +// - Return null for createIceServer if transport=tcp. +// - FF 27 and above supports transport parameters in TURN url, +// - So passing in the full url to create iceServer. +// Chrome: +// - Creates iceServer from the url for Chrome M33 and earlier. +// - Create iceServer with stun url. +// - Chrome M28 & above uses below TURN format. +// Plugin: +// - Creates Ice Server for Plugin Browsers +// - If Stun - Create iceServer with stun url. +// - Else - Create iceServer with turn url +// - This is a WebRTC Function +createIceServer = null; + +// Firefox: +// - Creates IceServers for Firefox +// - Use .url for FireFox. +// - Multiple Urls support +// Chrome: +// - Creates iceServers from the urls for Chrome M34 and above. +// - .urls is supported since Chrome M34. +// - Multiple Urls support +// Plugin: +// - Creates Ice Servers for Plugin Browsers +// - Multiple Urls support +// - This is a WebRTC Function +createIceServers = null; +//------------------------------------------------------------ + +//The RTCPeerConnection object. +RTCPeerConnection = null; + +// Creates RTCSessionDescription object for Plugin Browsers +RTCSessionDescription = (typeof RTCSessionDescription === 'function') ? + RTCSessionDescription : null; + +// Creates RTCIceCandidate object for Plugin Browsers +RTCIceCandidate = (typeof RTCIceCandidate === 'function') ? + RTCIceCandidate : null; + +// Get UserMedia (only difference is the prefix). +// Code from Adam Barth. +getUserMedia = null; + +// Attach a media stream to an element. +attachMediaStream = null; + +// Re-attach a media stream to an element. +reattachMediaStream = null; + + +// Detected browser agent name. Types are: +// - 'firefox': Firefox browser. +// - 'chrome': Chrome browser. +// - 'opera': Opera browser. +// - 'safari': Safari browser. +// - 'IE' - Internet Explorer browser. +webrtcDetectedBrowser = null; + +// Detected browser version. +webrtcDetectedVersion = null; + +// Check for browser types and react accordingly +if (navigator.mozGetUserMedia) { + webrtcDetectedBrowser = 'firefox'; + webrtcDetectedVersion = parseInt(navigator + .userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); + webrtcDetectedType = 'moz'; + webrtcDetectedDCSupport = 'SCTP'; + + RTCPeerConnection = function (pcConfig, pcConstraints) { + AdapterJS.maybeFixConfiguration(pcConfig); + return new mozRTCPeerConnection(pcConfig, pcConstraints); + }; + + // The RTCSessionDescription object. + RTCSessionDescription = mozRTCSessionDescription; + window.RTCSessionDescription = RTCSessionDescription; + + // The RTCIceCandidate object. + RTCIceCandidate = mozRTCIceCandidate; + window.RTCIceCandidate = RTCIceCandidate; + + window.getUserMedia = navigator.mozGetUserMedia.bind(navigator); + navigator.getUserMedia = window.getUserMedia; + + // Shim for MediaStreamTrack.getSources. + MediaStreamTrack.getSources = function(successCb) { + setTimeout(function() { + var infos = [ + { kind: 'audio', id: 'default', label:'', facing:'' }, + { kind: 'video', id: 'default', label:'', facing:'' } + ]; + successCb(infos); + }, 0); + }; + + createIceServer = function (url, username, password) { + var iceServer = null; + var url_parts = url.split(':'); + if (url_parts[0].indexOf('stun') === 0) { + iceServer = { url : url }; + } else if (url_parts[0].indexOf('turn') === 0) { + if (webrtcDetectedVersion < 27) { + var turn_url_parts = url.split('?'); + if (turn_url_parts.length === 1 || + turn_url_parts[1].indexOf('transport=udp') === 0) { + iceServer = { + url : turn_url_parts[0], + credential : password, + username : username + }; + } + } else { + iceServer = { + url : url, + credential : password, + username : username + }; + } + } + return iceServer; + }; + + createIceServers = function (urls, username, password) { + var iceServers = []; + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], username, password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + return iceServers; + }; + + attachMediaStream = function (element, stream) { + element.mozSrcObject = stream; + if (stream !== null) + element.play(); + + return element; + }; + + reattachMediaStream = function (to, from) { + to.mozSrcObject = from.mozSrcObject; + to.play(); + return to; + }; + + MediaStreamTrack.getSources = MediaStreamTrack.getSources || function (callback) { + if (!callback) { + throw new TypeError('Failed to execute \'getSources\' on \'MediaStreamTrack\'' + + ': 1 argument required, but only 0 present.'); + } + return callback([]); + }; + + // Fake get{Video,Audio}Tracks + if (!MediaStream.prototype.getVideoTracks) { + MediaStream.prototype.getVideoTracks = function () { + return []; + }; + } + if (!MediaStream.prototype.getAudioTracks) { + MediaStream.prototype.getAudioTracks = function () { + return []; + }; + } + + AdapterJS.maybeThroughWebRTCReady(); +} else if (navigator.webkitGetUserMedia) { + webrtcDetectedBrowser = 'chrome'; + webrtcDetectedType = 'webkit'; + webrtcDetectedVersion = parseInt(navigator + .userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); + // check if browser is opera 20+ + var checkIfOpera = navigator.userAgent.match(/\bOPR\/(\d+)/); + if (checkIfOpera !== null) { + webrtcDetectedBrowser = 'opera'; + webrtcDetectedVersion = parseInt(checkIfOpera[1], 10); + } + // check browser datachannel support + if ((webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion >= 31) || + (webrtcDetectedBrowser === 'opera' && webrtcDetectedVersion >= 20)) { + webrtcDetectedDCSupport = 'SCTP'; + } else if (webrtcDetectedBrowser === 'chrome' && webrtcDetectedVersion < 30 && + webrtcDetectedVersion > 24) { + webrtcDetectedDCSupport = 'RTP'; + } else { + webrtcDetectedDCSupport = ''; + } + + createIceServer = function (url, username, password) { + var iceServer = null; + var url_parts = url.split(':'); + if (url_parts[0].indexOf('stun') === 0) { + iceServer = { 'url' : url }; + } else if (url_parts[0].indexOf('turn') === 0) { + iceServer = { + 'url' : url, + 'credential' : password, + 'username' : username + }; + } + return iceServer; + }; + + createIceServers = function (urls, username, password) { + var iceServers = []; + if (webrtcDetectedVersion >= 34) { + iceServers = { + 'urls' : urls, + 'credential' : password, + 'username' : username + }; + } else { + for (i = 0; i < urls.length; i++) { + var iceServer = createIceServer(urls[i], username, password); + if (iceServer !== null) { + iceServers.push(iceServer); + } + } + } + return iceServers; + }; + + RTCPeerConnection = function (pcConfig, pcConstraints) { + if (webrtcDetectedVersion < 34) { + AdapterJS.maybeFixConfiguration(pcConfig); + } + return new webkitRTCPeerConnection(pcConfig, pcConstraints); + }; + + window.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + navigator.getUserMedia = window.getUserMedia; + + attachMediaStream = function (element, stream) { + if (typeof element.srcObject !== 'undefined') { + element.srcObject = stream; + } else if (typeof element.mozSrcObject !== 'undefined') { + element.mozSrcObject = stream; + } else if (typeof element.src !== 'undefined') { + element.src = (stream === null ? '' : URL.createObjectURL(stream)); + } else { + console.log('Error attaching stream to element.'); + } + return element; + }; + + reattachMediaStream = function (to, from) { + to.src = from.src; + return to; + }; + + AdapterJS.maybeThroughWebRTCReady(); +} else if (navigator.mediaDevices && navigator.userAgent.match( + /Edge\/(\d+).(\d+)$/)) { + webrtcDetectedBrowser = 'edge'; + + webrtcDetectedVersion = + parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10); + + // the minimum version still supported by adapter. + webrtcMinimumVersion = 12; + + window.getUserMedia = navigator.getUserMedia.bind(navigator); + + attachMediaStream = function(element, stream) { + element.srcObject = stream; + return element; + }; + reattachMediaStream = function(to, from) { + to.srcObject = from.srcObject; + return to; + }; + + AdapterJS.maybeThroughWebRTCReady(); +} else { // TRY TO USE PLUGIN + // IE 9 is not offering an implementation of console.log until you open a console + if (typeof console !== 'object' || typeof console.log !== 'function') { + /* jshint -W020 */ + console = {} || console; + // Implemented based on console specs from MDN + // You may override these functions + console.log = function (arg) {}; + console.info = function (arg) {}; + console.error = function (arg) {}; + console.dir = function (arg) {}; + console.exception = function (arg) {}; + console.trace = function (arg) {}; + console.warn = function (arg) {}; + console.count = function (arg) {}; + console.debug = function (arg) {}; + console.count = function (arg) {}; + console.time = function (arg) {}; + console.timeEnd = function (arg) {}; + console.group = function (arg) {}; + console.groupCollapsed = function (arg) {}; + console.groupEnd = function (arg) {}; + /* jshint +W020 */ + } + webrtcDetectedType = 'plugin'; + webrtcDetectedDCSupport = 'plugin'; + AdapterJS.parseWebrtcDetectedBrowser(); + isIE = webrtcDetectedBrowser === 'IE'; + + /* jshint -W035 */ + AdapterJS.WebRTCPlugin.WaitForPluginReady = function() { + while (AdapterJS.WebRTCPlugin.pluginState !== AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) { + /* empty because it needs to prevent the function from running. */ + } + }; + /* jshint +W035 */ + + AdapterJS.WebRTCPlugin.callWhenPluginReady = function (callback) { + if (AdapterJS.WebRTCPlugin.pluginState === AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) { + // Call immediately if possible + // Once the plugin is set, the code will always take this path + callback(); + } else { + // otherwise start a 100ms interval + var checkPluginReadyState = setInterval(function () { + if (AdapterJS.WebRTCPlugin.pluginState === AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) { + clearInterval(checkPluginReadyState); + callback(); + } + }, 100); + } + }; + + AdapterJS.WebRTCPlugin.setLogLevel = function(logLevel) { + AdapterJS.WebRTCPlugin.callWhenPluginReady(function() { + AdapterJS.WebRTCPlugin.plugin.setLogLevel(logLevel); + }); + }; + + AdapterJS.WebRTCPlugin.injectPlugin = function () { + // only inject once the page is ready + if (document.readyState !== 'complete') { + return; + } + + // Prevent multiple injections + if (AdapterJS.WebRTCPlugin.pluginState !== AdapterJS.WebRTCPlugin.PLUGIN_STATES.INITIALIZING) { + return; + } + + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INJECTING; + + if (webrtcDetectedBrowser === 'IE' && webrtcDetectedVersion <= 10) { + var frag = document.createDocumentFragment(); + AdapterJS.WebRTCPlugin.plugin = document.createElement('div'); + AdapterJS.WebRTCPlugin.plugin.innerHTML = '' + + ' ' + + ' ' + + ' ' + + '' + + '' + + // uncomment to be able to use virtual cams + (AdapterJS.options.getAllCams ? '':'') + + + ''; + while (AdapterJS.WebRTCPlugin.plugin.firstChild) { + frag.appendChild(AdapterJS.WebRTCPlugin.plugin.firstChild); + } + document.body.appendChild(frag); + + // Need to re-fetch the plugin + AdapterJS.WebRTCPlugin.plugin = + document.getElementById(AdapterJS.WebRTCPlugin.pluginInfo.pluginId); + } else { + // Load Plugin + AdapterJS.WebRTCPlugin.plugin = document.createElement('object'); + AdapterJS.WebRTCPlugin.plugin.id = + AdapterJS.WebRTCPlugin.pluginInfo.pluginId; + // IE will only start the plugin if it's ACTUALLY visible + if (isIE) { + AdapterJS.WebRTCPlugin.plugin.width = '1px'; + AdapterJS.WebRTCPlugin.plugin.height = '1px'; + } else { // The size of the plugin on Safari should be 0x0px + // so that the autorisation prompt is at the top + AdapterJS.WebRTCPlugin.plugin.width = '0px'; + AdapterJS.WebRTCPlugin.plugin.height = '0px'; + } + AdapterJS.WebRTCPlugin.plugin.type = AdapterJS.WebRTCPlugin.pluginInfo.type; + AdapterJS.WebRTCPlugin.plugin.innerHTML = '' + + '' + + ' ' + + (AdapterJS.options.getAllCams ? '':'') + + '' + + ''; + document.body.appendChild(AdapterJS.WebRTCPlugin.plugin); + } + + + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INJECTED; + }; + + AdapterJS.WebRTCPlugin.isPluginInstalled = + function (comName, plugName, installedCb, notInstalledCb) { + if (!isIE) { + var pluginArray = navigator.plugins; + for (var i = 0; i < pluginArray.length; i++) { + if (pluginArray[i].name.indexOf(plugName) >= 0) { + installedCb(); + return; + } + } + notInstalledCb(); + } else { + try { + var axo = new ActiveXObject(comName + '.' + plugName); + } catch (e) { + notInstalledCb(); + return; + } + installedCb(); + } + }; + + AdapterJS.WebRTCPlugin.defineWebRTCInterface = function () { + if (AdapterJS.WebRTCPlugin.pluginState === + AdapterJS.WebRTCPlugin.PLUGIN_STATES.READY) { + console.error("AdapterJS - WebRTC interface has already been defined"); + return; + } + + AdapterJS.WebRTCPlugin.pluginState = AdapterJS.WebRTCPlugin.PLUGIN_STATES.INITIALIZING; + + AdapterJS.isDefined = function (variable) { + return variable !== null && variable !== undefined; + }; + + createIceServer = function (url, username, password) { + var iceServer = null; + var url_parts = url.split(':'); + if (url_parts[0].indexOf('stun') === 0) { + iceServer = { + 'url' : url, + 'hasCredentials' : false + }; + } else if (url_parts[0].indexOf('turn') === 0) { + iceServer = { + 'url' : url, + 'hasCredentials' : true, + 'credential' : password, + 'username' : username + }; + } + return iceServer; + }; + + createIceServers = function (urls, username, password) { + var iceServers = []; + for (var i = 0; i < urls.length; ++i) { + iceServers.push(createIceServer(urls[i], username, password)); + } + return iceServers; + }; + + RTCSessionDescription = function (info) { + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + return AdapterJS.WebRTCPlugin.plugin. + ConstructSessionDescription(info.type, info.sdp); + }; + + RTCPeerConnection = function (servers, constraints) { + var iceServers = null; + if (servers) { + iceServers = servers.iceServers; + for (var i = 0; i < iceServers.length; i++) { + if (iceServers[i].urls && !iceServers[i].url) { + iceServers[i].url = iceServers[i].urls; + } + iceServers[i].hasCredentials = AdapterJS. + isDefined(iceServers[i].username) && + AdapterJS.isDefined(iceServers[i].credential); + } + } + var mandatory = (constraints && constraints.mandatory) ? + constraints.mandatory : null; + var optional = (constraints && constraints.optional) ? + constraints.optional : null; + + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + return AdapterJS.WebRTCPlugin.plugin. + PeerConnection(AdapterJS.WebRTCPlugin.pageId, + iceServers, mandatory, optional); + }; + + MediaStreamTrack = {}; + MediaStreamTrack.getSources = function (callback) { + AdapterJS.WebRTCPlugin.callWhenPluginReady(function() { + AdapterJS.WebRTCPlugin.plugin.GetSources(callback); + }); + }; + + window.getUserMedia = function (constraints, successCallback, failureCallback) { + constraints.audio = constraints.audio || false; + constraints.video = constraints.video || false; + + AdapterJS.WebRTCPlugin.callWhenPluginReady(function() { + AdapterJS.WebRTCPlugin.plugin. + getUserMedia(constraints, successCallback, failureCallback); + }); + }; + window.navigator.getUserMedia = window.getUserMedia; + + attachMediaStream = function (element, stream) { + if (!element || !element.parentNode) { + return; + } + + var streamId + if (stream === null) { + streamId = ''; + } + else { + stream.enableSoundTracks(true); // TODO: remove on 0.12.0 + streamId = stream.id; + } + + var elementId = element.id.length === 0 ? Math.random().toString(36).slice(2) : element.id; + var nodeName = element.nodeName.toLowerCase(); + if (nodeName !== 'object') { // not a plugin tag yet + var tag; + switch(nodeName) { + case 'audio': + tag = AdapterJS.WebRTCPlugin.TAGS.AUDIO; + break; + case 'video': + tag = AdapterJS.WebRTCPlugin.TAGS.VIDEO; + break; + default: + tag = AdapterJS.WebRTCPlugin.TAGS.NONE; + } + + var frag = document.createDocumentFragment(); + var temp = document.createElement('div'); + var classHTML = ''; + if (element.className) { + classHTML = 'class="' + element.className + '" '; + } else if (element.attributes && element.attributes['class']) { + classHTML = 'class="' + element.attributes['class'].value + '" '; + } + + temp.innerHTML = '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + while (temp.firstChild) { + frag.appendChild(temp.firstChild); + } + + var height = ''; + var width = ''; + if (element.getBoundingClientRect) { + var rectObject = element.getBoundingClientRect(); + width = rectObject.width + 'px'; + height = rectObject.height + 'px'; + } + else if (element.width) { + width = element.width; + height = element.height; + } else { + // TODO: What scenario could bring us here? + } + + element.parentNode.insertBefore(frag, element); + frag = document.getElementById(elementId); + frag.width = width; + frag.height = height; + element.parentNode.removeChild(element); + } else { // already an tag, just change the stream id + var children = element.children; + for (var i = 0; i !== children.length; ++i) { + if (children[i].name === 'streamId') { + children[i].value = streamId; + break; + } + } + element.setStreamId(streamId); + } + var newElement = document.getElementById(elementId); + newElement.onplaying = (element.onplaying) ? element.onplaying : function (arg) {}; + newElement.onplay = (element.onplay) ? element.onplay : function (arg) {}; + newElement.onclick = (element.onclick) ? element.onclick : function (arg) {}; + if (isIE) { // on IE the event needs to be plugged manually + newElement.attachEvent('onplaying', newElement.onplaying); + newElement.attachEvent('onplay', newElement.onplay); + newElement._TemOnClick = function (id) { + var arg = { + srcElement : document.getElementById(id) + }; + newElement.onclick(arg); + }; + } + + return newElement; + }; + + reattachMediaStream = function (to, from) { + var stream = null; + var children = from.children; + for (var i = 0; i !== children.length; ++i) { + if (children[i].name === 'streamId') { + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + stream = AdapterJS.WebRTCPlugin.plugin + .getStreamWithId(AdapterJS.WebRTCPlugin.pageId, children[i].value); + break; + } + } + if (stream !== null) { + return attachMediaStream(to, stream); + } else { + console.log('Could not find the stream associated with this element'); + } + }; + + RTCIceCandidate = function (candidate) { + if (!candidate.sdpMid) { + candidate.sdpMid = ''; + } + + AdapterJS.WebRTCPlugin.WaitForPluginReady(); + return AdapterJS.WebRTCPlugin.plugin.ConstructIceCandidate( + candidate.sdpMid, candidate.sdpMLineIndex, candidate.candidate + ); + }; + + // inject plugin + AdapterJS.addEvent(document, 'readystatechange', AdapterJS.WebRTCPlugin.injectPlugin); + AdapterJS.WebRTCPlugin.injectPlugin(); + }; + + // This function will be called if the plugin is needed (browser different + // from Chrome or Firefox), but the plugin is not installed. + AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb = AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb || + function() { + AdapterJS.addEvent(document, + 'readystatechange', + AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv); + AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv(); + }; + + AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCbPriv = function () { + if (AdapterJS.options.hidePluginInstallPrompt) { + return; + } + + var downloadLink = AdapterJS.WebRTCPlugin.pluginInfo.downloadLink; + if(downloadLink) { // if download link + var popupString; + if (AdapterJS.WebRTCPlugin.pluginInfo.portalLink) { // is portal link + popupString = 'This website requires you to install the ' + + ' ' + AdapterJS.WebRTCPlugin.pluginInfo.companyName + + ' WebRTC Plugin' + + ' to work on this browser.'; + } else { // no portal link, just print a generic explanation + popupString = AdapterJS.TEXT.PLUGIN.REQUIRE_INSTALLATION; + } + + AdapterJS.renderNotificationBar(popupString, AdapterJS.TEXT.PLUGIN.BUTTON, downloadLink); + } else { // no download link, just print a generic explanation + AdapterJS.renderNotificationBar(AdapterJS.TEXT.PLUGIN.NOT_SUPPORTED); + } + }; + + // Try to detect the plugin and act accordingly + AdapterJS.WebRTCPlugin.isPluginInstalled( + AdapterJS.WebRTCPlugin.pluginInfo.prefix, + AdapterJS.WebRTCPlugin.pluginInfo.plugName, + AdapterJS.WebRTCPlugin.defineWebRTCInterface, + AdapterJS.WebRTCPlugin.pluginNeededButNotInstalledCb); +} + +}).call(this,"/modules/RTC/adapter.screenshare.js") +},{"jitsi-meet-logger":45}],17:[function(require,module,exports){ +/* global config, APP, chrome, $, alert */ +/* jshint -W003 */ +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var AdapterJS = require("../RTC/adapter.screenshare"); +var DesktopSharingEventTypes + = require("../../service/desktopsharing/DesktopSharingEventTypes"); + +/** + * Indicates whether the Chrome desktop sharing extension is installed. + * @type {boolean} + */ +var chromeExtInstalled = false; + +/** + * Indicates whether an update of the Chrome desktop sharing extension is + * required. + * @type {boolean} + */ +var chromeExtUpdateRequired = false; + +/** + * Whether the jidesha extension for firefox is installed for the domain on + * which we are running. Null designates an unknown value. + * @type {null} + */ +var firefoxExtInstalled = null; + +/** + * If set to true, detection of an installed firefox extension will be started + * again the next time obtainScreenOnFirefox is called (e.g. next time the + * user tries to enable screen sharing). + */ +var reDetectFirefoxExtension = false; + +/** + * Handles obtaining a stream from a screen capture on different browsers. + */ +function ScreenObtainer(){ +} + +/** + * The EventEmitter to use to emit events. + * @type {null} + */ +ScreenObtainer.prototype.eventEmitter = null; + +/** + * Initializes the function used to obtain a screen capture (this.obtainStream). + * + * If the browser is Chrome, it uses the value of + * 'config.desktopSharingChromeMethod' (or 'config.desktopSharing') to * decide + * whether to use the a Chrome extension (if the value is 'ext'), use the + * "screen" media source (if the value is 'webrtc'), or disable screen capture + * (if the value is other). + * Note that for the "screen" media source to work the + * 'chrome://flags/#enable-usermedia-screen-capture' flag must be set. + */ +ScreenObtainer.prototype.init = function(eventEmitter) { + this.eventEmitter = eventEmitter; + var obtainDesktopStream = null; + + if (RTCBrowserType.isFirefox()) + initFirefoxExtensionDetection(); + + // TODO remove this, config.desktopSharing is deprecated. + var chromeMethod = + (config.desktopSharingChromeMethod || config.desktopSharing); + + if (RTCBrowserType.isTemasysPluginUsed()) { + if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) { + console.info("Screensharing not supported by this plugin version"); + } else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) { + console.info( + "Screensharing not available with Temasys plugin on this site"); + } else { + obtainDesktopStream = obtainWebRTCScreen; + console.info("Using Temasys plugin for desktop sharing"); + } + } else if (RTCBrowserType.isChrome()) { + if (chromeMethod == "ext") { + if (RTCBrowserType.getChromeVersion() >= 34) { + obtainDesktopStream = obtainScreenFromExtension; + console.info("Using Chrome extension for desktop sharing"); + initChromeExtension(); + } else { + console.info("Chrome extension not supported until ver 34"); + } + } else if (chromeMethod == "webrtc") { + obtainDesktopStream = obtainWebRTCScreen; + console.info("Using Chrome WebRTC for desktop sharing"); + } + } else if (RTCBrowserType.isFirefox()) { + if (config.desktopSharingFirefoxDisabled) { + obtainDesktopStream = null; + } else if (window.location.protocol === "http:"){ + console.log("Screen sharing is not supported over HTTP. Use of " + + "HTTPS is required."); + obtainDesktopStream = null; + } else { + obtainDesktopStream = this.obtainScreenOnFirefox; + } + + } + + if (!obtainDesktopStream) { + console.info("Desktop sharing disabled"); + } + + ScreenObtainer.prototype.obtainStream = obtainDesktopStream; +}; + +ScreenObtainer.prototype.obtainStream = null; + +/** + * Checks whether obtaining a screen capture is supported in the current + * environment. + * @returns {boolean} + */ +ScreenObtainer.prototype.isSupported = function() { + return !!this.obtainStream; +}; + +/** + * Obtains a desktop stream using getUserMedia. + * For this to work on Chrome, the + * 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled. + * + * On firefox, the document's domain must be white-listed in the + * 'media.getusermedia.screensharing.allowed_domains' preference in + * 'about:config'. + */ +function obtainWebRTCScreen(streamCallback, failCallback) { + APP.RTC.getUserMediaWithConstraints( + ['screen'], + streamCallback, + failCallback + ); +} + +/** + * Constructs inline install URL for Chrome desktop streaming extension. + * The 'chromeExtensionId' must be defined in config.js. + * @returns {string} + */ +function getWebStoreInstallUrl() +{ + //TODO remove chromeExtensionId (deprecated) + return "https://chrome.google.com/webstore/detail/" + + (config.desktopSharingChromeExtId || config.chromeExtensionId); +} + +/** + * Checks whether an update of the Chrome extension is required. + * @param minVersion minimal required version + * @param extVersion current extension version + * @returns {boolean} + */ +function isUpdateRequired(minVersion, extVersion) { + try { + var s1 = minVersion.split('.'); + var s2 = extVersion.split('.'); + + var len = Math.max(s1.length, s2.length); + for (var i = 0; i < len; i++) { + var n1 = 0, + n2 = 0; + + if (i < s1.length) + n1 = parseInt(s1[i]); + if (i < s2.length) + n2 = parseInt(s2[i]); + + if (isNaN(n1) || isNaN(n2)) { + return true; + } else if (n1 !== n2) { + return n1 > n2; + } + } + + // will happen if both versions have identical numbers in + // their components (even if one of them is longer, has more components) + return false; + } + catch (e) { + console.error("Failed to parse extension version", e); + APP.UI.messageHandler.showError("dialog.error", + "dialog.detectext"); + return true; + } +} + +function checkChromeExtInstalled(callback) { + if (!chrome || !chrome.runtime) { + // No API, so no extension for sure + callback(false, false); + return; + } + chrome.runtime.sendMessage( + //TODO: remove chromeExtensionId (deprecated) + (config.desktopSharingChromeExtId || config.chromeExtensionId), + { getVersion: true }, + function (response) { + if (!response || !response.version) { + // Communication failure - assume that no endpoint exists + console.warn( + "Extension not installed?: ", chrome.runtime.lastError); + callback(false, false); + return; + } + // Check installed extension version + var extVersion = response.version; + console.log('Extension version is: ' + extVersion); + //TODO: remove minChromeExtVersion (deprecated) + var updateRequired + = isUpdateRequired( + (config.desktopSharingChromeMinExtVersion || + config.minChromeExtVersion), + extVersion); + callback(!updateRequired, updateRequired); + } + ); +} + +function doGetStreamFromExtension(streamCallback, failCallback) { + // Sends 'getStream' msg to the extension. + // Extension id must be defined in the config. + chrome.runtime.sendMessage( + //TODO: remove chromeExtensionId (deprecated) + (config.desktopSharingChromeExtId || config.chromeExtensionId), + { + getStream: true, + //TODO: remove desktopSharingSources (deprecated). + sources: (config.desktopSharingChromeSources || + config.desktopSharingSources) + }, + function (response) { + if (!response) { + failCallback(chrome.runtime.lastError); + return; + } + console.log("Response from extension: " + response); + if (response.streamId) { + APP.RTC.getUserMediaWithConstraints( + ['desktop'], + function (stream) { + streamCallback(stream); + }, + failCallback, + null, null, null, + response.streamId); + } else { + failCallback("Extension failed to get the stream"); + } + } + ); +} + +/** + * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' + * stream for returned stream token. + */ +function obtainScreenFromExtension(streamCallback, failCallback) { + if (chromeExtInstalled) { + doGetStreamFromExtension(streamCallback, failCallback); + } else { + if (chromeExtUpdateRequired) { + alert( + 'Jitsi Desktop Streamer requires update. ' + + 'Changes will take effect after next Chrome restart.'); + } + + chrome.webstore.install( + getWebStoreInstallUrl(), + function (arg) { + console.log("Extension installed successfully", arg); + chromeExtInstalled = true; + // We need to give a moment for the endpoint to become available + window.setTimeout(function () { + doGetStreamFromExtension(streamCallback, failCallback); + }, 500); + }, + function (arg) { + console.log("Failed to install the extension", arg); + failCallback(arg); + APP.UI.messageHandler.showError("dialog.error", + "dialog.failtoinstall"); + } + ); + } +} + +/** + * Initializes with extension id set in + * config.js to support inline installs. Host site must be selected as main + * website of published extension. + */ +function initInlineInstalls() +{ + $("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl()); +} + +function initChromeExtension() { + // Initialize Chrome extension inline installs + initInlineInstalls(); + // Check if extension is installed + checkChromeExtInstalled(function (installed, updateRequired) { + chromeExtInstalled = installed; + chromeExtUpdateRequired = updateRequired; + console.info( + "Chrome extension installed: " + chromeExtInstalled + + " updateRequired: " + chromeExtUpdateRequired); + }); +} + +/** + * Obtains a screen capture stream on Firefox. + * @param callback + * @param errorCallback + */ +ScreenObtainer.prototype.obtainScreenOnFirefox = + function (callback, errorCallback) { + var self = this; + var extensionRequired = false; + if (config.desktopSharingFirefoxMaxVersionExtRequired === -1 || + (config.desktopSharingFirefoxMaxVersionExtRequired >= 0 && + RTCBrowserType.getFirefoxVersion() <= + config.desktopSharingFirefoxMaxVersionExtRequired)) { + extensionRequired = true; + console.log("Jidesha extension required on firefox version " + + RTCBrowserType.getFirefoxVersion()); + } + + if (!extensionRequired || firefoxExtInstalled === true) { + obtainWebRTCScreen(callback, errorCallback); + return; + } + + if (reDetectFirefoxExtension) { + reDetectFirefoxExtension = false; + initFirefoxExtensionDetection(); + } + + // Give it some (more) time to initialize, and assume lack of extension if + // it hasn't. + if (firefoxExtInstalled === null) { + window.setTimeout( + function() { + if (firefoxExtInstalled === null) + firefoxExtInstalled = false; + self.obtainScreenOnFirefox(callback, errorCallback); + }, + 300 + ); + console.log("Waiting for detection of jidesha on firefox to finish."); + return; + } + + // We need an extension and it isn't installed. + + // Make sure we check for the extension when the user clicks again. + firefoxExtInstalled = null; + reDetectFirefoxExtension = true; + + // Prompt the user to install the extension + this.eventEmitter.emit(DesktopSharingEventTypes.FIREFOX_EXTENSION_NEEDED, + config.desktopSharingFirefoxExtensionURL); + + // Make sure desktopsharing knows that we failed, so that it doesn't get + // stuck in 'switching' mode. + errorCallback('Firefox extension required.'); +}; + +/** + * Starts the detection of an installed jidesha extension for firefox. + */ +function initFirefoxExtensionDetection() { + if (config.desktopSharingFirefoxDisabled) { + return; + } + if (firefoxExtInstalled === false || firefoxExtInstalled === true) + return; + if (!config.desktopSharingFirefoxExtId) { + firefoxExtInstalled = false; + return; + } + + var img = document.createElement('img'); + img.onload = function(){ + console.log("Detected firefox screen sharing extension."); + firefoxExtInstalled = true; + }; + img.onerror = function(){ + console.log("Detected lack of firefox screen sharing extension."); + firefoxExtInstalled = false; + }; + + // The jidesha extension exposes an empty image file under the url: + // "chrome://EXT_ID/content/DOMAIN.png" + // Where EXT_ID is the ID of the extension with "@" replaced by ".", and + // DOMAIN is a domain whitelisted by the extension. + var src = "chrome://" + + (config.desktopSharingFirefoxExtId.replace('@', '.')) + + "/content/" + document.location.hostname + ".png"; + img.setAttribute('src', src); +} + +module.exports = ScreenObtainer; + +},{"../../service/desktopsharing/DesktopSharingEventTypes":81,"../RTC/RTCBrowserType":14,"../RTC/adapter.screenshare":16}],18:[function(require,module,exports){ +/* global APP, config */ +var EventEmitter = require("events"); +var DesktopSharingEventTypes + = require("../../service/desktopsharing/DesktopSharingEventTypes"); +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var RTCEvents = require("../../service/RTC/RTCEvents"); +var ScreenObtainer = require("./ScreenObtainer"); + +/** + * 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; + +/** + * Used to obtain the screen sharing stream from the browser. + */ +var screenObtainer = new ScreenObtainer(); + +var eventEmitter = new EventEmitter(); + +function streamSwitchDone() { + switchInProgress = false; + eventEmitter.emit( + DesktopSharingEventTypes.SWITCHING_DONE, + isUsingScreenStream); +} + +function newStreamCreated(stream) { + eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED, + stream, isUsingScreenStream, streamSwitchDone); +} + +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 onEndedHandler(stream) { + if (!switchInProgress && isUsingScreenStream) { + APP.desktopsharing.toggleScreenSharing(); + } + + APP.RTC.removeMediaStreamInactiveHandler(stream, onEndedHandler); +} + +module.exports = { + isUsingScreenStream: function () { + return isUsingScreenStream; + }, + + /** + * @returns {boolean} true if desktop sharing feature is available + * and enabled. + */ + isDesktopSharingEnabled: function () { + return screenObtainer.isSupported(); + }, + + init: function () { + // Called when RTC finishes initialization + APP.RTC.addListener(RTCEvents.RTC_READY, + function() { + screenObtainer.init(eventEmitter); + eventEmitter.emit(DesktopSharingEventTypes.INIT); + }); + }, + + addListener: function (type, listener) { + eventEmitter.on(type, listener); + }, + + removeListener: function (type, listener) { + eventEmitter.removeListener(type, listener); + }, + + /* + * Toggles screen sharing. + */ + toggleScreenSharing: function () { + if (switchInProgress) { + console.warn("Switch in progress."); + return; + } else if (!screenObtainer.isSupported()) { + console.warn("Cannot toggle screen sharing: not supported."); + return; + } + switchInProgress = true; + + if (!isUsingScreenStream) { + // Switch to desktop stream + screenObtainer.obtainStream( + function (stream) { + // We now use screen stream + isUsingScreenStream = true; + // Hook 'ended' event to restore camera + // when screen stream stops + APP.RTC.addMediaStreamInactiveHandler( + stream, onEndedHandler); + newStreamCreated(stream); + }, + getDesktopStreamFailed); + } else { + // Disable screen stream + APP.RTC.getUserMediaWithConstraints( + ['video'], + function (stream) { + // We are now using camera stream + isUsingScreenStream = false; + newStreamCreated(stream); + }, + getVideoStreamFailed, + config.resolution || '360' + ); + } + }, + /* + * Exports the event emitter to allow use by ScreenObtainer. Not for outside + * use. + */ + eventEmitter: eventEmitter +}; + + +},{"../../service/RTC/RTCEvents":77,"../../service/desktopsharing/DesktopSharingEventTypes":81,"../RTC/RTCBrowserType":14,"./ScreenObtainer":17,"events":41}],19:[function(require,module,exports){ +(function (__filename){ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +function supportsLocalStorage() { + try { + return 'localStorage' in window && window.localStorage !== null; + } catch (e) { + logger.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(); +} + +function Settings(conferenceID) { + this.email = ''; + this.displayName = ''; + this.userId; + this.language = null; + this.confSettings = null; + this.conferenceID = conferenceID; + if (supportsLocalStorage()) { + if(!window.localStorage.getItem(conferenceID)) + this.confSettings = {}; + else + this.confSettings = JSON.parse(window.localStorage.getItem(conferenceID)); + if(!this.confSettings.jitsiMeetId) { + this.confSettings.jitsiMeetId = generateUniqueId(); + logger.log("generated id", + this.confSettings.jitsiMeetId); + this.save(); + } + this.userId = this.confSettings.jitsiMeetId || ''; + this.email = this.confSettings.email || ''; + this.displayName = this.confSettings.displayname || ''; + this.language = this.confSettings.language; + } else { + logger.log("local storage is not supported"); + this.userId = generateUniqueId(); + } +} + +Settings.prototype.save = function () { + if(!supportsLocalStorage()) + window.localStorage.setItem(this.conferenceID, JSON.stringify(this.confSettings)); +} + +Settings.prototype.setDisplayName = function (newDisplayName) { + this.displayName = newDisplayName; + if(this.confSettings != null) + this.confSettings.displayname = displayName; + this.save(); + return this.displayName; +}, +Settings.prototype.setEmail = function (newEmail) { + this.email = newEmail; + if(this.confSettings != null) + this.confSettings.email = newEmail; + this.save(); + return this.email; +}, +Settings.prototype.getSettings = function () { + return { + email: this.email, + displayName: this.displayName, + uid: this.userId, + language: this.language + }; +}, +Settings.prototype.setLanguage = function (lang) { + this.language = lang; + if(this.confSettings != null) + this.confSettings.language = lang; + this.save(); +} + +module.exports = Settings; + +}).call(this,"/modules/settings/Settings.js") +},{"jitsi-meet-logger":45}],20:[function(require,module,exports){ +/* global config */ +/** + * Provides statistics for the local stream. + */ + +var RTCBrowserType = require('../RTC/RTCBrowserType'); +var StatisticsEvents = require('../../service/statistics/Events'); + +var LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID; + +/** + * Size of the webaudio analyzer buffer. + * @type {number} + */ +var WEBAUDIO_ANALYZER_FFT_SIZE = 2048; + +/** + * Value of the webaudio analyzer smoothing time parameter. + * @type {number} + */ +var WEBAUDIO_ANALYZER_SMOOTING_TIME = 0.8; + +/** + * Converts time domain data array to audio level. + * @param samples the time domain data array. + * @returns {number} the audio level + */ +function timeDomainDataToAudioLevel(samples) { + + var maxVolume = 0; + + var length = samples.length; + + for (var i = 0; i < length; i++) { + if (maxVolume < samples[i]) + maxVolume = samples[i]; + } + + return parseFloat(((maxVolume - 127) / 128).toFixed(3)); +} + +/** + * Animates audio level change + * @param newLevel the new audio level + * @param lastLevel the last audio level + * @returns {Number} the audio level to be set + */ +function animateLevel(newLevel, lastLevel) { + var value = 0; + var diff = lastLevel - newLevel; + if(diff > 0.2) { + value = lastLevel - 0.2; + } + else if(diff < -0.4) { + value = lastLevel + 0.4; + } + else { + value = newLevel; + } + + return parseFloat(value.toFixed(3)); +} + + +/** + * LocalStatsCollector calculates statistics for the local stream. + * + * @param stream the local stream + * @param interval stats refresh interval given in ms. + * @constructor + */ +function LocalStatsCollector(stream, interval, statisticsService, eventEmitter) { + window.AudioContext = window.AudioContext || window.webkitAudioContext; + this.stream = stream; + this.intervalId = null; + this.intervalMilis = interval; + this.eventEmitter = eventEmitter; + this.audioLevel = 0; + this.statisticsService = statisticsService; +} + +/** + * Starts the collecting the statistics. + */ +LocalStatsCollector.prototype.start = function () { + if (!window.AudioContext || + RTCBrowserType.isTemasysPluginUsed()) + return; + + var context = new AudioContext(); + var analyser = context.createAnalyser(); + analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME; + analyser.fftSize = WEBAUDIO_ANALYZER_FFT_SIZE; + + + var source = context.createMediaStreamSource(this.stream); + source.connect(analyser); + + + var self = this; + + this.intervalId = setInterval( + function () { + var array = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteTimeDomainData(array); + var audioLevel = timeDomainDataToAudioLevel(array); + if (audioLevel != self.audioLevel) { + self.audioLevel = animateLevel(audioLevel, self.audioLevel); + self.eventEmitter.emit( + StatisticsEvents.AUDIO_LEVEL, + self.statisticsService.LOCAL_JID, + self.audioLevel); + } + }, + this.intervalMilis + ); +}; + +/** + * Stops collecting the statistics. + */ +LocalStatsCollector.prototype.stop = function () { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } +}; + +module.exports = LocalStatsCollector; +},{"../../service/statistics/Events":82,"../../service/statistics/constants":83,"../RTC/RTCBrowserType":14}],21:[function(require,module,exports){ +(function (__filename){ +/* global require, ssrc2jid */ +/* jshint -W117 */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var StatisticsEvents = require("../../service/statistics/Events"); + +/* Whether we support the browser we are running into for logging statistics */ +var browserSupported = RTCBrowserType.isChrome() || + RTCBrowserType.isOpera(); + +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" +}; +keyMap[RTCBrowserType.RTC_BROWSER_OPERA] = + keyMap[RTCBrowserType.RTC_BROWSER_CHROME]; + +/** + * 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) { + var browserType = RTCBrowserType.getBrowserType(); + if (!keyMap[browserType][name]) + throw "The property isn't supported!"; + var key = keyMap[browserType][name]; + return (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) ? + item.stat(key) : item[key]; +} + +function formatAudioLevel(audioLevel) { + return Math.min(Math.max(audioLevel, 0), 1); +} + +/** + * 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; +} + +/** + * Peer statistics data holder. + * @constructor + */ +function PeerStats() +{ + this.ssrc2Loss = {}; + this.ssrc2AudioLevel = {}; + this.ssrc2bitrate = {}; + this.ssrc2resolution = {}; +} + +/** + * Sets packets loss rate for given ssrc that blong to the peer + * represented by this instance. + * @param lossRate new packet loss rate value to be set. + */ +PeerStats.prototype.setSsrcLoss = function (lossRate) +{ + this.ssrc2Loss = lossRate; +}; + +/** + * Sets resolution that belong to the ssrc + * represented by this instance. + * @param resolution new resolution value to be set. + */ +PeerStats.prototype.setSsrcResolution = function (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 ConferenceStats() { + + + /** + * The bandwidth + * @type {{}} + */ + this.bandwidth = {}; + + /** + * The bit rate + * @type {{}} + */ + this.bitrate = {}; + + /** + * The packet loss rate + * @type {{}} + */ + this.packetLoss = null; + + + /** + * Array with the transport information. + * @type {Array} + */ + this.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. + * @param config {object} supports the following properties - disableAudioLevels, disableStats, logStats + * @constructor + */ +function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter, config) +{ + this.peerconnection = peerconnection; + this.baselineAudioLevelsReport = null; + this.currentAudioLevelsReport = null; + this.currentStatsReport = null; + this.baselineStatsReport = null; + this.audioLevelsIntervalId = null; + this.eventEmitter = eventEmitter; + this.config = config || {}; + this.conferenceStats = new ConferenceStats(); + + /** + * 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 ssrcs to PeerStats + this.ssrc2stats = {}; +} + +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) +{ + logger.error("Get stats error", error); + this.stop(); +}; + +/** + * Starts stats updates. + */ +StatsCollector.prototype.start = function () +{ + var self = this; + 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(); + } + //logger.error("Got interval report", results); + self.currentAudioLevelsReport = results; + self.processAudioLevelReport(); + self.baselineAudioLevelsReport = + self.currentAudioLevelsReport; + }, + self.errorCallback + ); + }, + self.audioLevelsIntervalMilis + ); + +// if (!this.config.disableStats && browserSupported) { +// 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(); +// } +// //logger.error("Got interval report", results); +// self.currentStatsReport = results; +// try { +// self.processStatsReport(); +// } +// catch (e) { +// logger.error("Unsupported key:" + e, e); +// } +// +// self.baselineStatsReport = self.currentStatsReport; +// }, +// self.errorCallback +// ); +// }, +// self.statsIntervalMilis +// ); +// } +// +// if (this.config.logStats && browserSupported) { +// 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); +// } +}; + +/** + * 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)); + }); + }); +}; + + +//FIXME: +//StatsCollector.prototype.logStats = function () { +// +// if(!APP.xmpp.sendLogs(this.statsToBeLogged)) +// return; +// // Reset the stats +// this.statsToBeLogged.stats = {}; +// this.statsToBeLogged.timestamps = []; +//}; + + +/** + * 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')) { + this.conferenceStats.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 < this.conferenceStats.transport.length; i++) + { + if(this.conferenceStats.transport[i].ip == ip && + this.conferenceStats.transport[i].type == type && + this.conferenceStats.transport[i].localip == localIP) + { + addressSaved = true; + } + } + if(addressSaved) + continue; + this.conferenceStats.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]; + this.conferenceStats.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) { + logger.warn(getStatValue(now, 'ssrc') + ' not enough data'); + continue; + } + + var ssrc = getStatValue(now, 'ssrc'); + if(!ssrc) + continue; + + var ssrcStats = this.ssrc2stats[ssrc]; + if (!ssrcStats) { + ssrcStats = new PeerStats(); + this.ssrc2stats[ssrc] = ssrcStats; + } + + + var isDownloadStream = true; + var key = 'packetsReceived'; + var packetsNow = getStatValue(now, key); + if (typeof packetsNow === 'undefined' || packetsNow === null) { + isDownloadStream = false; + key = 'packetsSent'; + packetsNow = getStatValue(now, key); + if (typeof packetsNow === 'undefined' || packetsNow === null) { + console.warn("No packetsReceived nor packetsSent stat found"); + continue; + } + } + 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); + + ssrcStats.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); + } + + ssrcStats.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) { + ssrcStats.setSsrcResolution(ssrc, resolution); + } else { + ssrcStats.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.ssrc2stats).forEach( + function (jid) { + Object.keys(self.ssrc2stats[jid].ssrc2Loss).forEach( + function (ssrc) { + var type = "upload"; + if(self.ssrc2stats[jid].ssrc2Loss[ssrc].isDownloadStream) + type = "download"; + totalPackets[type] += + self.ssrc2stats[jid].ssrc2Loss[ssrc].packetsTotal; + lostPackets[type] += + self.ssrc2stats[jid].ssrc2Loss[ssrc].packetsLost; + } + ); + Object.keys(self.ssrc2stats[jid].ssrc2bitrate).forEach( + function (ssrc) { + bitrateDownload += + self.ssrc2stats[jid].ssrc2bitrate[ssrc].download; + bitrateUpload += + self.ssrc2stats[jid].ssrc2bitrate[ssrc].upload; + + delete self.ssrc2stats[jid].ssrc2bitrate[ssrc]; + } + ); + resolutions[jid] = self.ssrc2stats[jid].ssrc2resolution; + } + ); + + this.conferenceStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload}; + + this.conferenceStats.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(StatisticsEvents.CONNECTION_STATS, + { + "bitrate": this.conferenceStats.bitrate, + "packetLoss": this.conferenceStats.packetLoss, + "bandwidth": this.conferenceStats.bandwidth, + "resolution": resolutions, + "transport": this.conferenceStats.transport + }); + this.conferenceStats.transport = []; + +}; + +/** + * Stats processing logic. + */ +StatsCollector.prototype.processAudioLevelReport = function () { + if (!this.baselineAudioLevelsReport) { + return; + } + + for (var idx in this.currentAudioLevelsReport) { + var now = this.currentAudioLevelsReport[idx]; + + //if we don't have "packetsReceived" this is local stream + if (now.type != 'ssrc' || !getStatValue(now, 'packetsReceived')) { + continue; + } + + var before = this.baselineAudioLevelsReport[idx]; + if (!before) { + logger.warn(getStatValue(now, 'ssrc') + ' not enough data'); + continue; + } + + var ssrc = getStatValue(now, 'ssrc'); + if (!ssrc) { + if((Date.now() - now.timestamp) < 3000) + logger.warn("No ssrc: "); + continue; + } + + var ssrcStats = this.ssrc2stats[ssrc]; + if (!ssrcStats) { + ssrcStats = new PeerStats(); + this.ssrc2stats[ssrc] = ssrcStats; + } + + // Audio level + var audioLevel = null; + + try { + audioLevel = getStatValue(now, 'audioInputLevel'); + if (!audioLevel) + audioLevel = getStatValue(now, 'audioOutputLevel'); + } + catch(e) {/*not supported*/ + logger.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; + ssrcStats.setSsrcAudioLevel(ssrc, audioLevel); + this.eventEmitter.emit( + StatisticsEvents.AUDIO_LEVEL, ssrc, audioLevel); + } + } +}; + +}).call(this,"/modules/statistics/RTPStatsCollector.js") +},{"../../service/statistics/Events":82,"../RTC/RTCBrowserType":14,"jitsi-meet-logger":45}],22:[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 StreamEventTypes = require("../../service/RTC/StreamEventTypes.js"); +//var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +//var CallStats = require("./CallStats"); +//var RTCEvents = require("../../service/RTC/RTCEvents"); + +// +//function onDisposeConference(onUnload) { +// CallStats.sendTerminateEvent(); +// stopRemote(); +// if(onUnload) { +// stopLocal(); +// eventEmitter.removeAllListeners(); +// } +//} + +function Statistics() { + this.localStats = null; + this.rtpStats = null; + this.eventEmitter = new EventEmitter(); +} + +Statistics.prototype.startRemoteStats = function (peerconnection) { + if (this.rtpStats) { + this.rtpStats.stop(); + } + + this.rtpStats = new RTPStats(peerconnection, 200, 2000, this.eventEmitter); + this.rtpStats.start(); +} + +Statistics.prototype.startLocalStats = function (stream) { + if(stream.getType() !== "audio") + return; + this.localStats = new LocalStats(stream.getOriginalStream(), 200, this, + this.eventEmitter); + this.localStats.start(); +} + + +Statistics.prototype.addAudioLevelListener = function(listener) +{ + this.eventEmitter.on(StatisticsEvents.AUDIO_LEVEL, listener); +} + +Statistics.prototype.removeAudioLevelListener = function(listener) +{ + this.eventEmitter.removeListener(StatisticsEvents.AUDIO_LEVEL, listener); +} + +Statistics.prototype.dispose = function () { + this.stopLocal(); + this.stopRemote(); + if(this.eventEmitter) + { + this.eventEmitter.removeAllListeners(); + } +} + + +Statistics.prototype.stopLocal = function () { + if (this.localStats) { + this.localStats.stop(); + this.localStats = null; + } +} + +Statistics.prototype.stopRemote = function () { + if (this.rtpStats) { + this.rtpStats.stop(); + this.eventEmitter.emit(StatisticsEvents.STOP); + this.rtpStats = null; + } +} + +Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID; + +// +//var statistics = { +// /** +// * Indicates that this audio level is for local jid. +// * @type {string} +// */ +// LOCAL_JID: 'local', +// +// 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); +// }, +// +// +// stopRemoteStatistics: function() +// { +// stopRemote(); +// }, +// +//// Already implemented with the constructor +// start: function () { +// APP.RTC.addStreamListener(onStreamCreated, +// StreamEventTypes.EVENT_TYPE_LOCAL_CREATED); +// APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference); +// //FIXME: we may want to change CALL INCOMING event to onnegotiationneeded +// APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) { +// startRemoteStats(event.peerconnection); +//// CallStats.init(event); +// }); +//// APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY, function (session) { +//// CallStats.init(session); +//// }); +// //FIXME: that event is changed to TRACK_MUTE_CHANGED +//// APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) { +//// CallStats.sendMuteEvent(mute, "audio"); +//// }); +//// APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () { +//// CallStats.sendSetupFailedEvent(); +//// }); +// //FIXME: that event is changed to TRACK_MUTE_CHANGED +//// APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) { +//// CallStats.sendMuteEvent(mute, "video"); +//// }); +// } +//}; + + + + +module.exports = Statistics; +},{"../../service/statistics/Events":82,"../../service/statistics/constants":83,"./LocalStatsCollector.js":20,"./RTPStatsCollector.js":21,"events":41}],23:[function(require,module,exports){ +/** +/** + * @const + */ +var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +/** + * Hexadecimal digits. + * @const + */ +var HEX_DIGITS = '0123456789abcdef'; + +/** + * Generates random int within the range [min, max] + * @param min the minimum value for the generated number + * @param max the maximum value for the generated number + * @returns random int number + */ +function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Get random element from array or string. + * @param {Array|string} arr source + * @returns array element or string character + */ +function randomElement(arr) { + return arr[randomInt(0, arr.length - 1)]; +} + +/** + * Generate random alphanumeric string. + * @param {number} length expected string length + * @returns {string} random string of specified length + */ +function randomAlphanumStr(length) { + var result = ''; + + for (var i = 0; i < length; i += 1) { + result += randomElement(ALPHANUM); + } + + return result; +} + +/** + * Exported interface. + */ +var RandomUtil = { + /** + * Returns a random hex digit. + * @returns {*} + */ + randomHexDigit: function() { + return randomElement(HEX_DIGITS); + }, + /** + * Returns a random string of hex digits with length 'len'. + * @param len the length. + */ + randomHexString: function (len) { + var ret = ''; + while (len--) { + ret += this.randomHexDigit(); + } + return ret; + }, + randomElement: randomElement, + randomAlphanumStr: randomAlphanumStr +}; + +module.exports = RandomUtil; + +},{}],24:[function(require,module,exports){ +(function (__filename){ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var Moderator = require("./moderator"); +var EventEmitter = require("events"); + +var parser = { + packet2JSON: function (packet, nodes) { + var self = this; + $(packet).children().each(function (index) { + var tagName = $(this).prop("tagName"); + var node = {} + node["tagName"] = tagName; + node.attributes = {}; + $($(this)[0].attributes).each(function( index, attr ) { + node.attributes[ attr.name ] = attr.value; + } ); + var text = Strophe.getText($(this)[0]); + if(text) + node.value = text; + node.children = []; + nodes.push(node); + self.packet2JSON($(this), node.children); + }) + }, + JSON2packet: function (nodes, packet) { + for(var i = 0; i < nodes.length; i++) + { + var node = nodes[i]; + if(!node || node === null){ + continue; + } + packet.c(node.tagName, node.attributes); + if(node.value) + packet.t(node.value); + if(node.children) + this.JSON2packet(node.children, packet); + packet.up(); + } + // packet.up(); + } +}; + +/** + * Returns array of JS objects from the presence JSON associated with the passed nodeName + * @param pres the presence JSON + * @param nodeName the name of the node (videomuted, audiomuted, etc) + */ +function filterNodeFromPresenceJSON(pres, nodeName){ + var res = []; + for(var i = 0; i < pres.length; i++) + if(pres[i].tagName === nodeName) + res.push(pres[i]); + + return res; +} + +function ChatRoom(connection, jid, password, XMPP, options) { + this.eventEmitter = new EventEmitter(); + this.xmpp = XMPP; + this.connection = connection; + this.roomjid = Strophe.getBareJidFromJid(jid); + this.myroomjid = jid; + this.password = password; + logger.info("Joined MUC as " + this.myroomjid); + this.members = {}; + this.presMap = {}; + this.presHandlers = {}; + this.joined = false; + this.role = null; + this.focusMucJid = null; + this.bridgeIsDown = false; + this.options = options || {}; + this.moderator = new Moderator(this.roomjid, this.xmpp, this.eventEmitter); + this.initPresenceMap(); + this.session = null; + var self = this; + this.lastPresences = {}; +} + +ChatRoom.prototype.initPresenceMap = function () { + this.presMap['to'] = this.myroomjid; + this.presMap['xns'] = 'http://jabber.org/protocol/muc'; + this.presMap["nodes"] = []; + this.presMap["nodes"].push( { + "tagName": "user-agent", + "value": navigator.userAgent, + "attributes": {xmlns: 'http://jitsi.org/jitmeet/user-agent'} + }); + +}; + +ChatRoom.prototype.updateDeviceAvailability = function (devices) { + this.presMap["nodes"].push( { + "tagName": "devices", + "children": [ + { + "tagName": "audio", + "value": devices.audio, + }, + { + "tagName": "video", + "value": devices.video, + } + ] + }); +} + +ChatRoom.prototype.join = function (password, tokenPassword) { + if(password) + this.password = password; + var self = this; + this.moderator.allocateConferenceFocus(function() + { + self.sendPresence(tokenPassword); + }.bind(this)); +}; + +ChatRoom.prototype.sendPresence = function (tokenPassword) { + 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.password) { + pres.c('password').t(this.password).up(); + } + + pres.up(); + + // Send XEP-0115 'c' stanza that contains our capabilities info + if (this.connection.caps) { + this.connection.caps.node = this.xmpp.options.clientNode; + pres.c('c', this.connection.caps.generateCapsAttrs()).up(); + } + + if (tokenPassword) { + pres.c('token', { xmlns: 'http://jitsi.org/jitmeet/auth-token'}) + .t(tokenPassword).up(); + } + + parser.JSON2packet(this.presMap.nodes, pres); + this.connection.send(pres); +}; + + +ChatRoom.prototype.doLeave = function () { + logger.log("do leave", this.myroomjid); + var pres = $pres({to: this.myroomjid, type: 'unavailable' }); + this.presMap.length = 0; + this.connection.send(pres); +}; + + +ChatRoom.prototype.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) { + + logger.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) { + logger.error("Error getting room configuration form"); + }); +}; + +ChatRoom.prototype.onPresence = function (pres) { + var from = pres.getAttribute('from'); + // 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(this.moderator.getFocusUserJid() + "/") == 0) { + member.isFocus = true; + } + + $(pres).find(">x").remove(); + var nodes = []; + parser.packet2JSON(pres, nodes); + this.lastPresences[from] = nodes; + for(var i = 0; i < nodes.length; i++) + { + var node = nodes[i]; + switch(node.tagName) + { + case "nick": + member.nick = node.value; + if(!member.isFocus) { + var displayName = !this.xmpp.options.displayJids + ? member.nick : Strophe.getResourceFromJid(from); + + if (displayName && displayName.length > 0) { + this.eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName); + } + logger.info("Display name: " + displayName, pres); + } + break; + case "userId": + member.id = node.value; + break; + case "email": + member.email = node.value; + break; + case "bridgeIsDown": + if(!this.bridgeIsDown) { + this.bridgeIsDown = true; + this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN); + } + break; + default : + this.processNode(node, from); + } + + } + + if (from == this.myroomjid) { + if (member.affiliation == 'owner') + + if (this.role !== member.role) { + this.role = member.role; + + this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED, + member, this.isModerator()); + } + if (!this.joined) { + this.joined = true; + console.log("(TIME) MUC joined:\t", window.performance.now()); + this.eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member); + } + } else if (this.members[from] === undefined) { + // new participant + this.members[from] = member; + logger.log('entered', from, member); + if (member.isFocus) { + this.focusMucJid = from; + logger.info("Ignore focus: " + from + ", real JID: " + member.jid); + } + else { + this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, member.id || member.email, member.nick); + } + } else { + // Presence update for existing participant + // Watch role change: + if (this.members[from].role != member.role) { + this.members[from].role = member.role; + this.eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED, + member.role, member.nick); + } + + // store the new display name + if(member.displayName) + this.members[from].displayName = member.displayName; + } + + + + if(!member.isFocus) + this.eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, member.id || member.email); + + // Trigger status message update + if (member.status) { + this.eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member); + } + +}; + +ChatRoom.prototype.processNode = function (node, from) { + if(this.presHandlers[node.tagName]) + this.presHandlers[node.tagName](node, from); +}; + +ChatRoom.prototype.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); + this.eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body); +}; + +ChatRoom.prototype.setSubject = function (subject) { + var msg = $msg({to: this.roomjid, type: 'groupchat'}); + msg.c('subject', subject); + this.connection.send(msg); + logger.log("topic changed to " + subject); +}; + + +ChatRoom.prototype.onParticipantLeft = function (jid) { + + delete this.lastPresences[jid]; + this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid); + + this.moderator.onMucMemberLeft(jid); +}; + +ChatRoom.prototype.onPresenceUnavailable = function (pres, 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(); + } + + this.xmpp.disposeConference(false); + this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason); + delete this.connection.emuc.rooms[Strophe.getBareJidFromJid(jid)]; + return true; + } + + // Status code 110 indicates that this notification is "self-presence". + if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) { + delete this.members[from]; + this.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 (Object.keys(this.members).length > 1) { + for (var i in this.members) { + var member = this.members[i]; + delete this.members[i]; + this.onParticipantLeft(member); + } + } + if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) { + if (this.myroomjid === from) { + this.xmpp.disposeConference(false); + this.eventEmitter.emit(XMPPEvents.KICKED); + } + } +}; + +ChatRoom.prototype.onMessage = function (msg, 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") { + this.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 == "") { + this.eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText); + logger.log("Subject is changed to " + subjectText); + } + } + + // xep-0203 delay + var stamp = $(msg).find('>delay').attr('stamp'); + + if (!stamp) { + // or xep-0091 delay, UTC timestamp + stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp'); + + if (stamp) { + // the format is CCYYMMDDThh:mm:ss + var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/); + stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z"; + } + } + + if (txt) { + logger.log('chat', nick, txt); + this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED, + from, nick, txt, this.myroomjid, stamp); + } +} + +ChatRoom.prototype.onPresenceError = function (pres, from) { + if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) { + logger.log('on password required', from); + this.eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED); + } 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 === this.xmpp.options.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. + this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres); + + } else { + logger.warn('onPresError ', pres); + this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres); + } + } else { + logger.warn('onPresError ', pres); + this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres); + } +}; + +ChatRoom.prototype.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) { + logger.log('Kick participant with jid: ', jid, result); + }, + function (error) { + logger.log('Kick participant error: ', error); + }); +}; + +ChatRoom.prototype.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); +}; + +ChatRoom.prototype.addToPresence = function (key, values) { + values.tagName = key; + this.presMap["nodes"].push(values); +}; + +ChatRoom.prototype.removeFromPresence = function (key) { + for(var i = 0; i < this.presMap.nodes.length; i++) + { + if(key === this.presMap.nodes[i].tagName) + this.presMap.nodes.splice(i, 1); + } +}; + +ChatRoom.prototype.addPresenceListener = function (name, handler) { + this.presHandlers[name] = handler; +} + +ChatRoom.prototype.removePresenceListener = function (name) { + delete this.presHandlers[name]; +} + +ChatRoom.prototype.isModerator = function (jid) { + return this.role === 'moderator'; +}; + +ChatRoom.prototype.getMemberRole = function (peerJid) { + if (this.members[peerJid]) { + return this.members[peerJid].role; + } + return null; +}; + +ChatRoom.prototype.setJingleSession = function(session){ + this.session = session; + this.session.room = this; +}; + + +ChatRoom.prototype.removeStream = function (stream) { + if(!this.session) + return; + this.session.peerconnection.removeStream(stream) +} + +ChatRoom.prototype.switchStreams = function (stream, oldStream, callback, isAudio) { + if(this.session) { + // FIXME: will block switchInProgress on true value in case of exception + this.session.switchStreams(stream, oldStream, callback, isAudio); + } else { + // We are done immediately + logger.warn("No conference handler or conference not started yet"); + callback(); + } +}; + +ChatRoom.prototype.addStream = function (stream, callback) { + if(this.session) { + // FIXME: will block switchInProgress on true value in case of exception + this.session.addStream(stream, callback); + } else { + // We are done immediately + logger.warn("No conference handler or conference not started yet"); + callback(); + } +} + +ChatRoom.prototype.setVideoMute = function (mute, callback, options) { + var self = this; + var localCallback = function (mute) { + self.sendVideoInfoPresence(mute); + if(callback) + callback(mute) + }; + + if(this.session) + { + this.session.setVideoMute( + mute, localCallback, options); + } + else { + localCallback(mute); + } + +}; + +ChatRoom.prototype.setAudioMute = function (mute, callback) { + //This will be for remote streams only +// if (this.forceMuted && !mute) { +// logger.info("Asking focus for unmute"); +// this.connection.moderate.setMute(this.connection.emuc.myroomjid, mute); +// // FIXME: wait for result before resetting muted status +// this.forceMuted = false; +// } + + + return this.sendAudioInfoPresence(mute, callback); +}; + +ChatRoom.prototype.addAudioInfoToPresence = function (mute) { + this.removeFromPresence("audiomuted"); + this.addToPresence("audiomuted", + {attributes: + {"audions": "http://jitsi.org/jitmeet/audio"}, + value: mute.toString()}); +} + +ChatRoom.prototype.sendAudioInfoPresence = function(mute, callback) { + this.addAudioInfoToPresence(mute); + if(this.connection) { + this.sendPresence(); + } + if(callback) + callback(); +}; + +ChatRoom.prototype.addVideoInfoToPresence = function (mute) { + this.removeFromPresence("videomuted"); + this.addToPresence("videomuted", + {attributes: + {"videons": "http://jitsi.org/jitmeet/video"}, + value: mute.toString()}); +} + + +ChatRoom.prototype.sendVideoInfoPresence = function (mute) { + this.addVideoInfoToPresence(mute); + if(!this.connection) + return; + this.sendPresence(); +}; + +ChatRoom.prototype.addListener = function(type, listener) { + this.eventEmitter.on(type, listener); +}; + +ChatRoom.prototype.removeListener = function (type, listener) { + this.eventEmitter.removeListener(type, listener); +}; + +ChatRoom.prototype.remoteStreamAdded = function(data, sid, thessrc) { + if(this.lastPresences[data.peerjid]) + { + var pres = this.lastPresences[data.peerjid]; + var audiomuted = filterNodeFromPresenceJSON(pres, "audiomuted"); + var videomuted = filterNodeFromPresenceJSON(pres, "videomuted"); + data.videomuted = ((videomuted.length > 0 + && videomuted[0] + && videomuted[0]["value"] === "true")? true : false); + data.audiomuted = ((audiomuted.length > 0 + && audiomuted[0] + && audiomuted[0]["value"] === "true")? true : false); + } + + this.eventEmitter.emit(XMPPEvents.REMOTE_STREAM_RECEIVED, data, sid, thessrc); +} + +ChatRoom.prototype.getJidBySSRC = function (ssrc) { + if (!this.session) + return null; + return this.session.getSsrcOwner(ssrc); +}; + +module.exports = ChatRoom; + +}).call(this,"/modules/xmpp/ChatRoom.js") +},{"../../service/xmpp/XMPPEvents":84,"./moderator":32,"events":41,"jitsi-meet-logger":45}],25:[function(require,module,exports){ +(function (__filename){ +/* + * JingleSession provides an API to manage a single Jingle session. We will + * have different implementations depending on the underlying interface used + * (i.e. WebRTC and ORTC) and here we hold the code common to all of them. + */ +var logger = require("jitsi-meet-logger").getLogger(__filename); + +function JingleSession(me, sid, connection, service, eventEmitter) { + /** + * Our JID. + */ + this.me = me; + + /** + * The Jingle session identifier. + */ + this.sid = sid; + + /** + * The XMPP connection. + */ + this.connection = connection; + + /** + * The XMPP service. + */ + this.service = service; + + /** + * The event emitter. + */ + this.eventEmitter = eventEmitter; + + /** + * Whether to use dripping or not. Dripping is sending trickle candidates + * not one-by-one. + * Note: currently we do not support 'false'. + */ + this.usedrip = true; + + /** + * When dripping is used, stores ICE candidates which are to be sent. + */ + this.drip_container = []; + + // Media constraints. Is this WebRTC only? + this.media_constraints = null; + + // ICE servers config (RTCConfiguration?). + this.ice_config = {}; + + // The chat room instance associated with the session. + this.room = null; +} + +/** + * Prepares this object to initiate a session. + * @param peerjid the JID of the remote peer. + * @param isInitiator whether we will be the Jingle initiator. + * @param media_constraints + * @param ice_config + */ +JingleSession.prototype.initialize = function(peerjid, isInitiator, + media_constraints, ice_config) { + this.media_constraints = media_constraints; + this.ice_config = ice_config; + + if (this.state !== null) { + logger.error('attempt to initiate on session ' + this.sid + + 'in state ' + this.state); + return; + } + this.state = 'pending'; + this.initiator = isInitiator ? this.me : peerjid; + this.responder = !isInitiator ? this.me : peerjid; + this.peerjid = peerjid; + + this.doInitialize(); +}; + +/** + * Finishes initialization. + */ +JingleSession.prototype.doInitialize = function() {}; + +/** + * Adds the ICE candidates found in the 'contents' array as remote candidates? + * Note: currently only used on transport-info + */ +JingleSession.prototype.addIceCandidates = function(contents) {}; + +/** + * Handles an 'add-source' event. + * + * @param contents an array of Jingle 'content' elements. + */ +JingleSession.prototype.addSources = function(contents) {}; + +/** + * Handles a 'remove-source' event. + * + * @param contents an array of Jingle 'content' elements. + */ +JingleSession.prototype.removeSources = function(contents) {}; + +/** + * Terminates this Jingle session (stops sending media and closes the streams?) + */ +JingleSession.prototype.terminate = function() {}; + +/** + * Sends a Jingle session-terminate message to the peer and terminates the + * session. + * @param reason + * @param text + */ +JingleSession.prototype.sendTerminate = function(reason, text) {}; + +/** + * Handles an offer from the remote peer (prepares to accept a session). + * @param jingle the 'jingle' XML element. + */ +JingleSession.prototype.setOffer = function(jingle) {}; + +/** + * Handles an answer from the remote peer (prepares to accept a session). + * @param jingle the 'jingle' XML element. + */ +JingleSession.prototype.setAnswer = function(jingle) {}; + + +module.exports = JingleSession; + +}).call(this,"/modules/xmpp/JingleSession.js") +},{"jitsi-meet-logger":45}],26:[function(require,module,exports){ +(function (__filename){ +/* jshint -W117 */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var JingleSession = require("./JingleSession"); +var TraceablePeerConnection = require("./TraceablePeerConnection"); +var SDPDiffer = require("./SDPDiffer"); +var SDPUtil = require("./SDPUtil"); +var SDP = require("./SDP"); +var async = require("async"); +var transform = require("sdp-transform"); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var SSRCReplacement = require("./LocalSSRCReplacement"); +var RTC = require("../RTC/RTC"); + +// Jingle stuff +function JingleSessionPC(me, sid, connection, service) { + JingleSession.call(this, me, sid, connection, service); + this.initiator = null; + this.responder = null; + this.peerjid = null; + this.state = null; + this.localSDP = null; + this.remoteSDP = null; + this.relayedStreams = []; + + this.usetrickle = true; + this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718 + + this.hadstuncandidate = false; + this.hadturncandidate = false; + this.lasticecandidate = false; + + this.statsinterval = null; + + this.reason = null; + + this.addssrc = []; + this.removessrc = []; + this.pendingop = null; + this.switchstreams = false; + this.addingStreams = false; + + this.wait = true; + this.localStreamsSSRC = null; + this.ssrcOwners = {}; + this.ssrcVideoTypes = {}; + + this.webrtcIceUdpDisable = !!this.service.options.webrtcIceUdpDisable; + this.webrtcIceTcpDisable = !!this.service.options.webrtcIceTcpDisable; + + /** + * The indicator which determines whether the (local) video has been muted + * in response to a user command in contrast to an automatic decision made + * by the application logic. + */ + this.videoMuteByUser = false; + + this.modifySourcesQueue = async.queue(this._modifySources.bind(this), 1); + // We start with the queue paused. We resume it when the signaling state is + // stable and the ice connection state is connected. + this.modifySourcesQueue.pause(); +} +//XXX this is badly broken... +JingleSessionPC.prototype = JingleSession.prototype; +JingleSessionPC.prototype.constructor = JingleSessionPC; + + +JingleSessionPC.prototype.setOffer = function(offer) { + this.setRemoteDescription(offer, 'offer'); +}; + +JingleSessionPC.prototype.setAnswer = function(answer) { + this.setRemoteDescription(answer, 'answer'); +}; + +JingleSessionPC.prototype.updateModifySourcesQueue = function() { + var signalingState = this.peerconnection.signalingState; + var iceConnectionState = this.peerconnection.iceConnectionState; + if (signalingState === 'stable' && iceConnectionState === 'connected') { + this.modifySourcesQueue.resume(); + } else { + this.modifySourcesQueue.pause(); + } +}; + +JingleSessionPC.prototype.doInitialize = function () { + var self = this; + + this.hadstuncandidate = false; + this.hadturncandidate = false; + this.lasticecandidate = false; + // True if reconnect is in progress + this.isreconnect = false; + // Set to true if the connection was ever stable + this.wasstable = false; + + this.peerconnection = new TraceablePeerConnection( + this.connection.jingle.ice_config, + RTC.getPCConstraints(), + this); + + this.peerconnection.onicecandidate = function (event) { + var protocol; + if (event && event.candidate) { + protocol = (typeof event.candidate.protocol === 'string') + ? event.candidate.protocol.toLowerCase() : ''; + if ((self.webrtcIceTcpDisable && protocol == 'tcp') || + (self.webrtcIceUdpDisable && protocol == 'udp')) { + return; + } + } + self.sendIceCandidate(event.candidate); + }; + this.peerconnection.onaddstream = function (event) { + if (event.stream.id !== 'default') { + logger.log("REMOTE STREAM ADDED: ", event.stream , event.stream.id); + self.remoteStreamAdded(event); + } else { + // This is a recvonly stream. Clients that implement Unified Plan, + // such as Firefox use recvonly "streams/channels/tracks" for + // receiving remote stream/tracks, as opposed to Plan B where there + // are only 3 channels: audio, video and data. + logger.log("RECVONLY REMOTE STREAM IGNORED: " + event.stream + " - " + event.stream.id); + } + }; + this.peerconnection.onremovestream = function (event) { + // Remove the stream from remoteStreams + // FIXME: remotestreamremoved.jingle not defined anywhere(unused) + + $(document).trigger('remotestreamremoved.jingle', [event, self.sid]); + }; + this.peerconnection.onsignalingstatechange = function (event) { + if (!(self && self.peerconnection)) return; + if (self.peerconnection.signalingState === 'stable') { + self.wasstable = true; + } + self.updateModifySourcesQueue(); + }; + /** + * The oniceconnectionstatechange event handler contains the code to execute when the iceconnectionstatechange event, + * of type Event, is received by this RTCPeerConnection. Such an event is sent when the value of + * RTCPeerConnection.iceConnectionState changes. + * + * @param event the event containing information about the change + */ + this.peerconnection.oniceconnectionstatechange = function (event) { + if (!(self && self.peerconnection)) return; + logger.log("(TIME) ICE " + self.peerconnection.iceConnectionState + + ":\t", window.performance.now()); + self.updateModifySourcesQueue(); + switch (self.peerconnection.iceConnectionState) { + case 'connected': + + // Informs interested parties that the connection has been restored. + if (self.peerconnection.signalingState === 'stable' && self.isreconnect) + self.room.eventEmitter.emit(XMPPEvents.CONNECTION_RESTORED); + self.isreconnect = false; + + break; + case 'disconnected': + self.isreconnect = true; + // Informs interested parties that the connection has been interrupted. + if (self.wasstable) + self.room.eventEmitter.emit(XMPPEvents.CONNECTION_INTERRUPTED); + break; + case 'failed': + self.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + break; + } + onIceConnectionStateChange(self.sid, self); + }; + this.peerconnection.onnegotiationneeded = function (event) { + self.room.eventEmitter.emit(XMPPEvents.PEERCONNECTION_READY, self); + }; + + this.relayedStreams.forEach(function(stream) { + self.peerconnection.addStream(stream); + }); +}; + +function onIceConnectionStateChange(sid, session) { + switch (session.peerconnection.iceConnectionState) { + case 'checking': + session.timeChecking = (new Date()).getTime(); + session.firstconnect = true; + break; + case 'completed': // on caller side + case 'connected': + if (session.firstconnect) { + session.firstconnect = false; + var metadata = {}; + metadata.setupTime + = (new Date()).getTime() - session.timeChecking; + session.peerconnection.getStats(function (res) { + if(res && res.result) { + res.result().forEach(function (report) { + if (report.type == 'googCandidatePair' && + report.stat('googActiveConnection') == 'true') { + metadata.localCandidateType + = report.stat('googLocalCandidateType'); + metadata.remoteCandidateType + = report.stat('googRemoteCandidateType'); + + // log pair as well so we can get nice pie + // charts + metadata.candidatePair + = report.stat('googLocalCandidateType') + + ';' + + report.stat('googRemoteCandidateType'); + + if (report.stat('googRemoteAddress').indexOf('[') === 0) + { + metadata.ipv6 = true; + } + } + }); + } + }); + } + break; + } +} + +JingleSessionPC.prototype.accept = function () { + this.state = 'active'; + + var pranswer = this.peerconnection.localDescription; + if (!pranswer || pranswer.type != 'pranswer') { + return; + } + logger.log('going from pranswer to answer'); + if (this.usetrickle) { + // remove candidates already sent from session-accept + var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:'); + for (var i = 0; i < lines.length; i++) { + pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', ''); + } + } + while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) { + // FIXME: change any inactive to sendrecv or whatever they were originally + pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv'); + } + var prsdp = new SDP(pranswer.sdp); + if (self.webrtcIceTcpDisable) { + prsdp.removeTcpCandidates = true; + } + if (self.webrtcIceUdpDisable) { + prsdp.removeUdpCandidates = true; + } + var accept = $iq({to: this.peerjid, + type: 'set'}) + .c('jingle', {xmlns: 'urn:xmpp:jingle:1', + action: 'session-accept', + initiator: this.initiator, + responder: this.responder, + sid: this.sid }); + // FIXME why do we generate session-accept in 3 different places ? + prsdp.toJingle( + accept, + this.initiator == this.me ? 'initiator' : 'responder', + this.localStreamsSSRC); + var sdp = this.peerconnection.localDescription.sdp; + while (SDPUtil.find_line(sdp, 'a=inactive')) { + // FIXME: change any inactive to sendrecv or whatever they were originally + sdp = sdp.replace('a=inactive', 'a=sendrecv'); + } + var self = this; + this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}), + function () { + //logger.log('setLocalDescription success'); + self.setLocalDescription(); + + SSRCReplacement.processSessionInit(accept); + + 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'; + JingleSessionPC.onJingleError(self.sid, error); + }, + 10000); + }, + function (e) { + logger.error('setLocalDescription failed', e); + self.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + } + ); +}; + +JingleSessionPC.prototype.terminate = function (reason) { + this.state = 'ended'; + this.reason = reason; + this.peerconnection.close(); + if (this.statsinterval !== null) { + window.clearInterval(this.statsinterval); + this.statsinterval = null; + } +}; + +JingleSessionPC.prototype.active = function () { + return this.state == 'active'; +}; + +JingleSessionPC.prototype.sendIceCandidate = function (candidate) { + var self = this; + if (candidate && !this.lasticecandidate) { + var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session); + var jcand = SDPUtil.candidateToJingle(candidate.candidate); + if (!(ice && jcand)) { + logger.error('failed to get ice && jcand'); + return; + } + ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; + + if (jcand.type === 'srflx') { + this.hadstuncandidate = true; + } else if (jcand.type === 'relay') { + this.hadturncandidate = true; + } + + if (this.usetrickle) { + if (this.usedrip) { + if (this.drip_container.length === 0) { + // start 20ms callout + window.setTimeout(function () { + if (self.drip_container.length === 0) return; + self.sendIceCandidates(self.drip_container); + self.drip_container = []; + }, 20); + + } + this.drip_container.push(candidate); + return; + } else { + self.sendIceCandidate([candidate]); + } + } + } else { + //logger.log('sendIceCandidate: last candidate.'); + if (!this.usetrickle) { + //logger.log('should send full offer now...'); + //FIXME why do we generate session-accept in 3 different places ? + var init = $iq({to: this.peerjid, + type: 'set'}) + .c('jingle', {xmlns: 'urn:xmpp:jingle:1', + action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept', + initiator: this.initiator, + sid: this.sid}); + this.localSDP = new SDP(this.peerconnection.localDescription.sdp); + if (self.webrtcIceTcpDisable) { + this.localSDP.removeTcpCandidates = true; + } + if (self.webrtcIceUdpDisable) { + this.localSDP.removeUdpCandidates = true; + } + var sendJingle = function (ssrc) { + if(!ssrc) + ssrc = {}; + self.localSDP.toJingle( + init, + self.initiator == self.me ? 'initiator' : 'responder', + ssrc); + + SSRCReplacement.processSessionInit(init); + + self.connection.sendIQ(init, + function () { + //logger.log('session initiate ack'); + 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'; + JingleSessionPC.onJingleError(self.sid, error); + }, + 10000); + }; + sendJingle(); + } + this.lasticecandidate = true; + logger.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate); + logger.log('Have we encountered any relay candidates? ' + this.hadturncandidate); + + if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') { + $(document).trigger('nostuncandidates.jingle', [this.sid]); + } + } +}; + +JingleSessionPC.prototype.sendIceCandidates = function (candidates) { + logger.log('sendIceCandidates', candidates); + var cand = $iq({to: this.peerjid, type: 'set'}) + .c('jingle', {xmlns: 'urn:xmpp:jingle:1', + action: 'transport-info', + initiator: this.initiator, + sid: this.sid}); + for (var mid = 0; mid < this.localSDP.media.length; mid++) { + var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; }); + var mline = SDPUtil.parse_mline(this.localSDP.media[mid].split('\r\n')[0]); + if (cands.length > 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 + //logger.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'; + JingleSessionPC.onJingleError(this.sid, error); + }, + 10000); +}; + + +JingleSessionPC.prototype.sendOffer = function () { + //logger.log('sendOffer...'); + var self = this; + this.peerconnection.createOffer(function (sdp) { + self.createdOffer(sdp); + }, + function (e) { + logger.error('createOffer failed', e); + }, + this.media_constraints + ); +}; + +// FIXME createdOffer is never used in jitsi-meet +JingleSessionPC.prototype.createdOffer = function (sdp) { + //logger.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); + + SSRCReplacement.processSessionInit(init); + + 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'; + JingleSessionPC.onJingleError(self.sid, error); + }, + 10000); + } + sdp.sdp = this.localSDP.raw; + this.peerconnection.setLocalDescription(sdp, + function () { + if(self.usetrickle) + { + sendJingle(); + } + self.setLocalDescription(); + //logger.log('setLocalDescription success'); + }, + function (e) { + logger.error('setLocalDescription failed', e); + self.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + } + ); + 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; + } + } +}; + +JingleSessionPC.prototype.readSsrcInfo = function (contents) { + var self = this; + $(contents).each(function (idx, content) { + var name = $(content).attr('name'); + var mediaType = this.getAttribute('name'); + var ssrcs = $(content).find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); + ssrcs.each(function () { + var ssrc = this.getAttribute('ssrc'); + $(this).find('>ssrc-info[xmlns="http://jitsi.org/jitmeet"]').each( + function () { + var owner = this.getAttribute('owner'); + self.ssrcOwners[ssrc] = owner; + } + ); + }); + }); +}; + +JingleSessionPC.prototype.getSsrcOwner = function (ssrc) { + return this.ssrcOwners[ssrc]; +}; + +JingleSessionPC.prototype.setRemoteDescription = function (elem, desctype) { + //logger.log('setting remote description... ', desctype); + this.remoteSDP = new SDP(''); + if (self.webrtcIceTcpDisable) { + this.remoteSDP.removeTcpCandidates = true; + } + if (self.webrtcIceUdpDisable) { + this.remoteSDP.removeUdpCandidates = true; + } + + this.remoteSDP.fromJingle(elem); + this.readSsrcInfo($(elem).find(">content")); + if (this.peerconnection.remoteDescription) { + logger.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 { + logger.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 { + logger.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 () { + //logger.log('setRemoteDescription success'); + }, + function (e) { + logger.error('setRemoteDescription error', e); + JingleSessionPC.onJingleFatalError(self, e); + } + ); +}; + +/** + * Adds remote ICE candidates to this Jingle session. + * @param elem An array of Jingle "content" elements? + */ +JingleSessionPC.prototype.addIceCandidate = function (elem) { + var self = this; + if (this.peerconnection.signalingState == 'closed') { + return; + } + if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') { + logger.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 { + logger.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) { + logger.log('setting pranswer'); + try { + this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }), + function() { + }, + function(e) { + logger.log('setRemoteDescription pranswer failed', e.toString()); + }); + } catch (e) { + logger.error('setting pranswer failed', e); + } + } else { + //logger.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; + var protocol = this.getAttribute('protocol'); + protocol = + (typeof protocol === 'string') ? protocol.toLowerCase() : ''; + if ((self.webrtcIceTcpDisable && protocol == 'tcp') || + (self.webrtcIceUdpDisable && protocol == 'udp')) { + return; + } + + line = SDPUtil.candidateFromJingle(this); + candidate = new RTCIceCandidate({sdpMLineIndex: idx, + sdpMid: name, + candidate: line}); + try { + self.peerconnection.addIceCandidate(candidate); + } catch (e) { + logger.error('addIceCandidate failed', e.toString(), line); + } + }); + }); +}; + +JingleSessionPC.prototype.sendAnswer = function (provisional) { + //logger.log('createAnswer', provisional); + var self = this; + this.peerconnection.createAnswer( + function (sdp) { + self.createdAnswer(sdp, provisional); + }, + function (e) { + logger.error('createAnswer failed', e); + self.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + }, + this.media_constraints + ); +}; + +JingleSessionPC.prototype.createdAnswer = function (sdp, provisional) { + //logger.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) { + // FIXME why do we generate session-accept in 3 different places ? + 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 }); + if (self.webrtcIceTcpDisable) { + self.localSDP.removeTcpCandidates = true; + } + if (self.webrtcIceUdpDisable) { + self.localSDP.removeUdpCandidates = true; + } + self.localSDP.toJingle( + accept, + self.initiator == self.me ? 'initiator' : 'responder', + ssrcs); + + SSRCReplacement.processSessionInit(accept); + + 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'; + JingleSessionPC.onJingleError(self.sid, error); + }, + 10000); + } + sdp.sdp = this.localSDP.raw; + this.peerconnection.setLocalDescription(sdp, + function () { + + //logger.log('setLocalDescription success'); + if (self.usetrickle && !self.usepranswer) { + sendJingle(); + } + self.setLocalDescription(); + }, + function (e) { + logger.error('setLocalDescription failed', e); + self.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + } + ); + 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; + } + } +}; + +JingleSessionPC.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; + } +}; + +/** + * Handles a Jingle source-add message for this Jingle session. + * @param elem An array of Jingle "content" elements. + */ +JingleSessionPC.prototype.addSource = function (elem) { + + var self = this; + // FIXME: dirty waiting + if (!this.peerconnection.localDescription) + { + logger.warn("addSource - localDescription not ready yet") + setTimeout(function() + { + self.addSource(elem); + }, + 200 + ); + return; + } + + logger.log('addssrc', new Date().getTime()); + logger.log('ice', this.peerconnection.iceConnectionState); + + this.readSsrcInfo(elem); + + 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) { + 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 + */ + logger.warn("Got add stream request for my own ssrc: "+ssrc); + return; + } + if (sdp.containsSSRC(ssrc)) { + logger.warn("Source-add request for existing 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. + logger.log('modify sources done'); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + logger.log("SDPs", mySdp, newSdp); + self.notifyMySSRCUpdate(mySdp, newSdp); + }); +}; + +/** + * Handles a Jingle source-remove message for this Jingle session. + * @param elem An array of Jingle "content" elements. + */ +JingleSessionPC.prototype.removeSource = function (elem) { + + var self = this; + // FIXME: dirty waiting + if (!this.peerconnection.localDescription) { + logger.warn("removeSource - localDescription not ready yet"); + setTimeout(function() { + self.removeSource(elem); + }, + 200 + ); + return; + } + + logger.log('removessrc', new Date().getTime()); + logger.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) { + 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)){ + logger.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. + logger.log('modify sources done'); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + logger.log("SDPs", mySdp, newSdp); + self.notifyMySSRCUpdate(mySdp, newSdp); + }); +}; + +JingleSessionPC.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 || this.addingStreams)){ + // 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 flags + this.switchstreams = false; + this.addingStreams = 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 = []; + + sdp.raw = sdp.session + sdp.media.join(''); + this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}), + function() { + + if(self.signalingState == 'closed') { + logger.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... + //logger.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() { + //logger.log('modified setLocalDescription ok'); + self.setLocalDescription(); + if(successCallback){ + successCallback(); + } + queueCallback(); + }, + function(error) { + logger.error('modified setLocalDescription failed', error); + queueCallback(error); + } + ); + }, + function(error) { + logger.error('modified answer failed', error); + queueCallback(error); + } + ); + }, + function(error) { + logger.error('modify failed', error); + queueCallback(error); + } + ); +}; + + +/** + * Switches video streams. + * @param newStream new stream that will be used as video of this session. + * @param oldStream old video stream of this session. + * @param successCallback callback executed after successful stream switch. + * @param isAudio whether the streams are audio (if true) or video (if false). + */ +JingleSessionPC.prototype.switchStreams = + function (newStream, oldStream, successCallback, isAudio) { + var self = this; + var sender, newTrack; + var senderKind = isAudio ? 'audio' : 'video'; + // 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); + } + if (RTCBrowserType.getBrowserType() === + RTCBrowserType.RTC_BROWSER_FIREFOX) { + // On Firefox we don't replace MediaStreams as this messes up the + // m-lines (which can't be removed in Plan Unified) and brings a lot + // of complications. Instead, we use the RTPSender and replace just + // the track. + + // Find the right sender (for audio or video) + self.peerconnection.peerconnection.getSenders().some(function (s) { + if (s.track && s.track.kind === senderKind) { + sender = s; + return true; + } + }); + + if (sender) { + // We assume that our streams have a single track, either audio + // or video. + newTrack = isAudio ? newStream.getAudioTracks()[0] : + newStream.getVideoTracks()[0]; + sender.replaceTrack(newTrack) + .then(function() { + console.log("Replaced a track, isAudio=" + isAudio); + }) + .catch(function(err) { + console.log("Failed to replace a track: " + err); + }); + } else { + console.log("Cannot switch tracks: no RTPSender."); + } + } else { + self.peerconnection.removeStream(oldStream, true); + if (newStream) { + self.peerconnection.addStream(newStream); + } + } + } + + // Conference is not active + if (!oldSdp) { + successCallback(); + return; + } + + self.switchstreams = true; + self.modifySourcesQueue.push(function() { + logger.log('modify sources done'); + + successCallback(); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + logger.log("SDPs", oldSdp, newSdp); + self.notifyMySSRCUpdate(oldSdp, newSdp); + }); +}; + +/** + * Adds streams. + * @param stream new stream that will be added. + * @param success_callback callback executed after successful stream addition. + */ +JingleSessionPC.prototype.addStream = function (stream, callback) { + + var self = this; + + // Remember SDP to figure out added/removed SSRCs + var oldSdp = null; + if(this.peerconnection) { + if(this.peerconnection.localDescription) { + oldSdp = new SDP(this.peerconnection.localDescription.sdp); + } + if(stream) + this.peerconnection.addStream(stream); + } + + // Conference is not active + if(!oldSdp || !this.peerconnection) { + callback(); + return; + } + + this.addingStreams = true; + this.modifySourcesQueue.push(function() { + logger.log('modify sources done'); + + callback(); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + logger.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. + */ +JingleSessionPC.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { + + if (!(this.peerconnection.signalingState == 'stable' && + this.peerconnection.iceConnectionState == 'connected')){ + logger.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); + + // Let 'source-remove' IQ through the hack and see if we're allowed to send + // it in the current form + if (removed) + remove = SSRCReplacement.processSourceRemove(remove); + + if (removed && remove) { + logger.info("Sending source-remove", remove); + this.connection.sendIQ(remove, + function (res) { + logger.info('got remove result', res); + }, + function (err) { + logger.error('got remove error', err); + } + ); + } else { + logger.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); + + // Let 'source-add' IQ through the hack and see if we're allowed to send + // it in the current form + if (added) + add = SSRCReplacement.processSourceAdd(add); + + if (added && add) { + logger.info("Sending source-add", add); + this.connection.sendIQ(add, + function (res) { + logger.info('got add result', res); + }, + function (err) { + logger.error('got add error', err); + } + ); + } else { + logger.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) + */ +JingleSessionPC.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() { + logger.log('modify sources done'); + + callback(mute); + + var newSdp = new SDP(self.peerconnection.localDescription.sdp); + logger.log("SDPs", oldSdp, newSdp); + self.notifyMySSRCUpdate(oldSdp, newSdp); + }); +}; + +JingleSessionPC.prototype.hardMuteVideo = function (muted) { + this.pendingop = muted ? 'mute' : 'unmute'; +}; + +JingleSessionPC.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); +}; + +JingleSessionPC.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); +}; + +JingleSessionPC.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; +}; + +JingleSessionPC.onJingleError = function (session, error) +{ + logger.error("Jingle error", error); +} + +JingleSessionPC.onJingleFatalError = function (session, error) +{ + this.room.eventEmitter.emit(XMPPEvents.CONFERENCE_SETUP_FAILED); + this.room.eventEmitter.emit(XMPPEvents.JINGLE_FATAL_ERROR, session, error); +} + +JingleSessionPC.prototype.setLocalDescription = function () { + var self = this; + var newssrcs = []; + if(!this.peerconnection.localDescription) + return; + var session = transform.parse(this.peerconnection.localDescription.sdp); + var i; + session.media.forEach(function (media) { + + if (media.ssrcs && 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 + }); + }); + } + else if(self.localStreamsSSRC && self.localStreamsSSRC[media.type]) + { + newssrcs.push({ + 'ssrc': self.localStreamsSSRC[media.type], + 'type': media.type + }); + } + + }); + + logger.log('new ssrcs', newssrcs); + + // Bind us as local SSRCs owner + if (newssrcs.length > 0) { + for (i = 0; i < newssrcs.length; i++) { + var ssrc = newssrcs[i].ssrc; + var myJid = self.connection.emuc.myroomjid; + self.ssrcOwners[ssrc] = myJid; + } + } +} + +// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 +JingleSessionPC.prototype.sendKeyframe = function () { + var pc = this.peerconnection; + logger.log('sendkeyframe', pc.iceConnectionState); + if (pc.iceConnectionState !== 'connected') return; // safe... + var self = this; + pc.setRemoteDescription( + pc.remoteDescription, + function () { + pc.createAnswer( + function (modifiedAnswer) { + pc.setLocalDescription( + modifiedAnswer, + function () { + // noop + }, + function (error) { + logger.log('triggerKeyframe setLocalDescription failed', error); + self.room.eventEmitter.emit(XMPPEvents.SET_LOCAL_DESCRIPTION_ERROR); + } + ); + }, + function (error) { + logger.log('triggerKeyframe createAnswer failed', error); + self.room.eventEmitter.emit(XMPPEvents.CREATE_ANSWER_ERROR); + } + ); + }, + function (error) { + logger.log('triggerKeyframe setRemoteDescription failed', error); + eventEmitter.emit(XMPPEvents.SET_REMOTE_DESCRIPTION_ERROR); + } + ); +} + + +JingleSessionPC.prototype.remoteStreamAdded = function (data, times) { + var self = this; + var thessrc; + var streamId = RTC.getStreamID(data.stream); + + // look up an associated JID for a stream id + if (!streamId) { + logger.error("No stream ID for", data.stream); + } else if (streamId && streamId.indexOf('mixedmslabel') === -1) { + // look only at a=ssrc: and _not_ at a=ssrc-group: lines + + var ssrclines = this.peerconnection.remoteDescription? + 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; + + if (RTCBrowserType.isTemasysPluginUsed()) { + return ((line.indexOf('mslabel:' + streamId) !== -1)); + } else { + return ((line.indexOf('msid:' + streamId) !== -1)); + } + }); + if (ssrclines.length) { + thessrc = ssrclines[0].substring(7).split(' ')[0]; + + if (!self.ssrcOwners[thessrc]) { + logger.error("No SSRC owner known for: " + thessrc); + return; + } + data.peerjid = self.ssrcOwners[thessrc]; + logger.log('associated jid', self.ssrcOwners[thessrc]); + } else { + logger.error("No SSRC lines for ", streamId); + } + } + + this.room.remoteStreamAdded(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 && + RTC.localVideo.getTracks().length > 0) { + window.setTimeout(function () { + self.sendKeyframe(); + }, 3000); + } +} + +module.exports = JingleSessionPC; + +}).call(this,"/modules/xmpp/JingleSessionPC.js") +},{"../../service/xmpp/XMPPEvents":84,"../RTC/RTC":13,"../RTC/RTCBrowserType":14,"./JingleSession":25,"./LocalSSRCReplacement":27,"./SDP":28,"./SDPDiffer":29,"./SDPUtil":30,"./TraceablePeerConnection":31,"async":40,"jitsi-meet-logger":45,"sdp-transform":73}],27:[function(require,module,exports){ +(function (__filename){ +/* global $ */ +var logger = require("jitsi-meet-logger").getLogger(__filename); + + +/* + Here we do modifications of local video SSRCs. There are 2 situations we have + to handle: + + 1. We generate SSRC for local recvonly video stream. This is the case when we + have no local camera and it is not generated automatically, but SSRC=1 is + used implicitly. If that happens RTCP packets will be dropped by the JVB + and we won't be able to request video key frames correctly. + + 2. A hack to re-use SSRC of the first video stream for any new stream created + in future. It turned out that Chrome may keep on using the SSRC of removed + video stream in RTCP even though a new one has been created. So we just + want to avoid that by re-using it. Jingle 'source-remove'/'source-add' + notifications are blocked once first video SSRC is advertised to the focus. + + What this hack does: + + 1. Stores the SSRC of the first video stream created by + a) scanning Jingle session-accept/session-invite for existing video SSRC + b) watching for 'source-add' for new video stream if it has not been + created in step a) + 2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with + the stored one. It is called by 'TracablePeerConnection' before local SDP is + returned to the other parts of the application. + 3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and + blocks those notifications. This makes Jicofo and all participants think + that it exists all the time even if the video stream has been removed or + replaced locally. Thanks to that there is no additional signaling activity + on video mute or when switching to the desktop stream. + */ + +var SDP = require('./SDP'); +var RandomUtil = require('../util/RandomUtil'); +var RTCBrowserType = require('../RTC/RTCBrowserType'); + +/** + * The hack is enabled on all browsers except FF by default + * FIXME finish the hack once removeStream method is implemented in FF + * @type {boolean} + */ +var isEnabled = !RTCBrowserType.isFirefox(); + + +/** + * Stored SSRC of local video stream. + */ +var localVideoSSRC; + +/** + * SSRC, msid, mslabel, label used for recvonly video stream when we have no local camera. + * This is in order to tell Chrome what SSRC should be used in RTCP requests + * instead of 1. + */ +var localRecvOnlySSRC, localRecvOnlyMSID, localRecvOnlyMSLabel, localRecvOnlyLabel; + +/** + * cname for localRecvOnlySSRC + */ +var localRecvOnlyCName; + +/** + * Method removes element which describes localVideoSSRC + * from given Jingle IQ. + * @param modifyIq 'source-add' or 'source-remove' Jingle IQ. + * @param actionName display name of the action which will be printed in log + * messages. + * @returns {*} modified Jingle IQ, so that it does not contain element + * corresponding to localVideoSSRC or null if no + * other SSRCs left to be signaled after removing it. + */ +var filterOutSource = function (modifyIq, actionName) { + var modifyIqTree = $(modifyIq.tree()); + + if (!localVideoSSRC) + return modifyIqTree[0]; + + var videoSSRC = modifyIqTree.find( + '>jingle>content[name="video"]' + + '>description>source[ssrc="' + localVideoSSRC + '"]'); + + if (!videoSSRC.length) { + return modifyIqTree[0]; + } + + logger.info( + 'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC); + + videoSSRC.remove(); + + // Check if any sources still left to be added/removed + if (modifyIqTree.find('>jingle>content>description>source').length) { + return modifyIqTree[0]; + } else { + return null; + } +}; + +/** + * Scans given Jingle IQ for video SSRC and stores it. + * @param jingleIq the Jingle IQ to be scanned for video SSRC. + */ +var storeLocalVideoSSRC = function (jingleIq) { + var videoSSRCs = + $(jingleIq.tree()) + .find('>jingle>content[name="video"]>description>source'); + + videoSSRCs.each(function (idx, ssrcElem) { + if (localVideoSSRC) + return; + // We consider SSRC real only if it has msid attribute + // recvonly streams in FF do not have it as well as local SSRCs + // we generate for recvonly streams in Chrome + var ssrSel = $(ssrcElem); + var msid = ssrSel.find('>parameter[name="msid"]'); + if (msid.length) { + var ssrcVal = ssrSel.attr('ssrc'); + if (ssrcVal) { + localVideoSSRC = ssrcVal; + logger.info('Stored local video SSRC' + + ' for future re-use: ' + localVideoSSRC); + } + } + }); +}; + +/** + * Generates new label/mslabel attribute + * @returns {string} label/mslabel attribute + */ +function generateLabel() { + return RandomUtil.randomHexString(8) + "-" + RandomUtil.randomHexString(4) + + "-" + RandomUtil.randomHexString(4) + "-" + + RandomUtil.randomHexString(4) + "-" + RandomUtil.randomHexString(12); +} + +/** + * Generates new SSRC, CNAME, mslabel, label and msid for local video recvonly stream. + * FIXME what about eventual SSRC collision ? + */ +function generateRecvonlySSRC() { + localRecvOnlySSRC = + Math.random().toString(10).substring(2, 11); + localRecvOnlyCName = + Math.random().toString(36).substring(2); + localRecvOnlyMSLabel = generateLabel(); + localRecvOnlyLabel = generateLabel(); + localRecvOnlyMSID = localRecvOnlyMSLabel + " " + localRecvOnlyLabel; + + + logger.info( + "Generated local recvonly SSRC: " + localRecvOnlySSRC + + ", cname: " + localRecvOnlyCName); +} + +var LocalSSRCReplacement = { + /** + * Method must be called before 'session-initiate' or 'session-invite' is + * sent. Scans the IQ for local video SSRC and stores it if detected. + * + * @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ + * which will be scanned for local video SSRC. + */ + processSessionInit: function (sessionInit) { + if (!isEnabled) + return; + + if (localVideoSSRC) { + logger.error("Local SSRC stored already: " + localVideoSSRC); + return; + } + storeLocalVideoSSRC(sessionInit); + }, + /** + * If we have local video SSRC stored searched given + * localDescription for video SSRC and makes sure it is replaced + * with the stored one. + * @param localDescription local description object that will have local + * video SSRC replaced with the stored one + * @returns modified localDescription object. + */ + mungeLocalVideoSSRC: function (localDescription) { + if (!isEnabled) + return localDescription; + + if (!localDescription) { + logger.warn("localDescription is null or undefined"); + return localDescription; + } + + // IF we have local video SSRC stored make sure it is replaced + // with old SSRC + if (localVideoSSRC) { + var newSdp = new SDP(localDescription.sdp); + if (newSdp.media[1].indexOf("a=ssrc:") !== -1 && + !newSdp.containsSSRC(localVideoSSRC)) { + // Get new video SSRC + var map = newSdp.getMediaSsrcMap(); + var videoPart = map[1]; + var videoSSRCs = videoPart.ssrcs; + var newSSRC = Object.keys(videoSSRCs)[0]; + + logger.info( + "Replacing new video SSRC: " + newSSRC + + " with " + localVideoSSRC); + + localDescription.sdp = + newSdp.raw.replace( + new RegExp('a=ssrc:' + newSSRC, 'g'), + 'a=ssrc:' + localVideoSSRC); + } + } else { + // Make sure we have any SSRC for recvonly video stream + var sdp = new SDP(localDescription.sdp); + + if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 && + sdp.media[1].indexOf('a=recvonly') !== -1) { + + if (!localRecvOnlySSRC) { + generateRecvonlySSRC(); + } + localVideoSSRC = localRecvOnlySSRC; + + logger.info('No SSRC in video recvonly stream' + + ' - adding SSRC: ' + localRecvOnlySSRC); + + sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC + + ' cname:' + localRecvOnlyCName + '\r\n' + + 'a=ssrc:' + localRecvOnlySSRC + + ' msid:' + localRecvOnlyMSID + '\r\n' + + 'a=ssrc:' + localRecvOnlySSRC + + ' mslabel:' + localRecvOnlyMSLabel + '\r\n' + + 'a=ssrc:' + localRecvOnlySSRC + + ' label:' + localRecvOnlyLabel + '\r\n'; + + localDescription.sdp = sdp.session + sdp.media.join(''); + } + } + return localDescription; + }, + /** + * Method must be called before 'source-add' notification is sent. In case + * we have local video SSRC advertised already it will be removed from the + * notification. If no other SSRCs are described by given IQ null will be + * returned which means that there is no point in sending the notification. + * @param sourceAdd 'source-add' Jingle IQ to be processed + * @returns modified 'source-add' IQ which can be sent to the focus or + * null if no notification shall be sent. It is no longer + * a Strophe IQ Builder instance, but DOM element tree. + */ + processSourceAdd: function (sourceAdd) { + if (!isEnabled) + return sourceAdd; + + if (!localVideoSSRC) { + // Store local SSRC if available + storeLocalVideoSSRC(sourceAdd); + return sourceAdd; + } else { + return filterOutSource(sourceAdd, 'source-add'); + } + }, + /** + * Method must be called before 'source-remove' notification is sent. + * Removes local video SSRC from the notification. If there are no other + * SSRCs described in the given IQ null will be returned which + * means that there is no point in sending the notification. + * @param sourceRemove 'source-remove' Jingle IQ to be processed + * @returns modified 'source-remove' IQ which can be sent to the focus or + * null if no notification shall be sent. It is no longer + * a Strophe IQ Builder instance, but DOM element tree. + */ + processSourceRemove: function (sourceRemove) { + if (!isEnabled) + return sourceRemove; + + return filterOutSource(sourceRemove, 'source-remove'); + }, + + /** + * Turns the hack on or off + * @param enabled true to enable the hack or false + * to disable it + */ + setEnabled: function (enabled) { + isEnabled = enabled; + } +}; + +module.exports = LocalSSRCReplacement; + +}).call(this,"/modules/xmpp/LocalSSRCReplacement.js") +},{"../RTC/RTCBrowserType":14,"../util/RandomUtil":23,"./SDP":28,"jitsi-meet-logger":45}],28:[function(require,module,exports){ +(function (__filename){ +/* jshint -W117 */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var SDPUtil = require("./SDPUtil"); + +// SDP STUFF +function SDP(sdp) { + /** + * Whether or not to remove TCP ice candidates when translating from/to jingle. + * @type {boolean} + */ + this.removeTcpCandidates = false; + + /** + * Whether or not to remove UDP ice candidates when translating from/to jingle. + * @type {boolean} + */ + this.removeUdpCandidates = false; + + this.media = sdp.split('\r\nm='); + for (var i = 1; i < this.media.length; i++) { + this.media[i] = 'm=' + this.media[i]; + if (i != this.media.length - 1) { + this.media[i] += '\r\n'; + } + } + this.session = this.media.shift() + '\r\n'; + this.raw = this.session + this.media.join(''); +} + +/** + * Returns map of MediaChannel mapped per channel idx. + */ +SDP.prototype.getMediaSsrcMap = function() { + var self = this; + var media_ssrcs = {}; + var tmp; + for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) { + tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:'); + var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:')); + var media = { + mediaindex: mediaindex, + mid: mid, + ssrcs: {}, + ssrcGroups: [] + }; + media_ssrcs[mediaindex] = media; + tmp.forEach(function (line) { + var linessrc = line.substring(7).split(' ')[0]; + // allocate new ChannelSsrc + if(!media.ssrcs[linessrc]) { + media.ssrcs[linessrc] = { + ssrc: linessrc, + lines: [] + }; + } + media.ssrcs[linessrc].lines.push(line); + }); + tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:'); + tmp.forEach(function(line){ + var idx = line.indexOf(' '); + var semantics = line.substr(0, idx).substr(13); + var ssrcs = line.substr(14 + semantics.length).split(' '); + if (ssrcs.length) { + media.ssrcGroups.push({ + semantics: semantics, + ssrcs: ssrcs + }); + } + }); + } + return media_ssrcs; +}; +/** + * Returns 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(); + Object.keys(medias).forEach(function(mediaindex){ + var media = medias[mediaindex]; + //logger.log("Check", channel, ssrc); + if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){ + return true; + } + }); + return false; +}; + +// 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) { +// logger.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]); + var self = this; + var i, j, k, mline, ssrc, rtpmap, tmp, lines; + // new bundle plan + if (SDPUtil.find_line(this.session, 'a=group:')) { + lines = SDPUtil.find_lines(this.session, 'a=group:'); + for (i = 0; i < lines.length; i++) { + tmp = lines[i].split(' '); + var semantics = tmp.shift().substr(8); + elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics}); + for (j = 0; j < tmp.length; j++) { + elem.c('content', {name: tmp[j]}).up(); + } + elem.up(); + } + } + for (i = 0; i < this.media.length; i++) { + mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]); + if (!(mline.media === 'audio' || + mline.media === 'video' || + mline.media === 'application')) + { + continue; + } + if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) { + ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first + } else { + 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) { + var idx = line.indexOf(' '); + var linessrc = line.substr(0, idx).substr(7); + if (linessrc != ssrc) { + elem.up(); + ssrc = linessrc; + elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); + } + var kv = line.substr(idx + 1); + elem.c('parameter'); + if (kv.indexOf(':') == -1) { + elem.attrs({ name: kv }); + } else { + var k = kv.split(':', 2)[0]; + elem.attrs({ name: k }); + + var v = kv.split(':', 2)[1]; + v = SDPUtil.filter_special_chars(v); + elem.attrs({ value: v }); + } + elem.up(); + }); + } else { + elem.up(); + elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); + elem.c('parameter'); + elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)}); + elem.up(); + var msid = null; + if(mline.media == "audio") + { + msid = APP.RTC.localAudio.getId(); + } + else + { + msid = APP.RTC.localVideo.getId(); + } + if(msid != null) + { + msid = SDPUtil.filter_special_chars(msid); + elem.c('parameter'); + elem.attrs({name: "msid", value:msid}); + elem.up(); + elem.c('parameter'); + elem.attrs({name: "mslabel", value:msid}); + elem.up(); + elem.c('parameter'); + elem.attrs({name: "label", value:msid}); + elem.up(); + } + } + elem.up(); + + // XEP-0339 handle ssrc-group attributes + var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:'); + ssrc_group_lines.forEach(function(line) { + var idx = line.indexOf(' '); + var semantics = line.substr(0, idx).substr(13); + var ssrcs = line.substr(14 + semantics.length).split(' '); + if (ssrcs.length) { + elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); + ssrcs.forEach(function(ssrc) { + elem.c('source', { ssrc: ssrc }) + .up(); + }); + elem.up(); + } + }); + } + + if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) { + elem.c('rtcp-mux').up(); + } + + // XEP-0293 -- map a=rtcp-fb:* + this.rtcpFbToJingle(i, elem, '*'); + + // XEP-0294 + if (SDPUtil.find_line(this.media[i], 'a=extmap:')) { + lines = SDPUtil.find_lines(this.media[i], 'a=extmap:'); + for (j = 0; j < lines.length; j++) { + tmp = SDPUtil.parse_extmap(lines[j]); + elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0', + uri: tmp.uri, + id: tmp.value }); + if (tmp.hasOwnProperty('direction')) { + switch (tmp.direction) { + case 'sendonly': + elem.attrs({senders: 'responder'}); + break; + case 'recvonly': + elem.attrs({senders: 'initiator'}); + break; + case 'sendrecv': + elem.attrs({senders: 'both'}); + break; + case 'inactive': + elem.attrs({senders: 'none'}); + break; + } + } + // TODO: handle params + elem.up(); + } + } + elem.up(); // end of description + } + + // map ice-ufrag/pwd, dtls fingerprint, candidates + this.transportToJingle(i, elem); + + if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) { + elem.attrs({senders: 'both'}); + } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) { + elem.attrs({senders: 'initiator'}); + } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) { + elem.attrs({senders: 'responder'}); + } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) { + elem.attrs({senders: 'none'}); + } + if (mline.port == '0') { + // estos hack to reject an m-line + elem.attrs({senders: 'rejected'}); + } + elem.up(); // end of content + } + elem.up(); + return elem; +}; + +SDP.prototype.transportToJingle = function (mediaindex, elem) { + var tmp, sctpmap, sctpAttrs, fingerprints; + var self = this; + elem.c('transport'); + + // XEP-0343 DTLS/SCTP + if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length) + { + sctpmap = SDPUtil.find_line( + this.media[mediaindex], 'a=sctpmap:', self.session); + if (sctpmap) + { + sctpAttrs = SDPUtil.parse_sctpmap(sctpmap); + elem.c('sctpmap', + { + xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1', + number: sctpAttrs[0], /* SCTP port */ + protocol: sctpAttrs[1] /* protocol */ + }); + // Optional stream count attribute + if (sctpAttrs.length > 2) + elem.attrs({ streams: sctpAttrs[2]}); + elem.up(); + } + } + // XEP-0320 + fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session); + fingerprints.forEach(function(line) { + tmp = SDPUtil.parse_fingerprint(line); + tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; + elem.c('fingerprint').t(tmp.fingerprint); + delete tmp.fingerprint; + line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session); + if (line) { + tmp.setup = line.substr(8); + } + elem.attrs(tmp); + elem.up(); // end of fingerprint + }); + tmp = SDPUtil.iceparams(this.media[mediaindex], this.session); + if (tmp) { + tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1'; + elem.attrs(tmp); + // XEP-0176 + if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines + var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session); + lines.forEach(function (line) { + var candidate = SDPUtil.candidateToJingle(line); + var protocol = (candidate && + typeof candidate.protocol === 'string') + ? candidate.protocol.toLowerCase() : ''; + if ((self.removeTcpCandidates && protocol === 'tcp') || + (self.removeUdpCandidates && protocol === 'udp')) { + return; + } + elem.c('candidate', candidate).up(); + }); + } + } + elem.up(); // end of transport +} + +SDP.prototype.rtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293 + var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype); + lines.forEach(function (line) { + var tmp = SDPUtil.parse_rtcpfb(line); + if (tmp.type == 'trr-int') { + elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]}); + elem.up(); + } else { + elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type}); + if (tmp.params.length > 0) { + elem.attrs({'subtype': tmp.params[0]}); + } + elem.up(); + } + }); +}; + +SDP.prototype.rtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293 + var media = ''; + var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]'); + if (tmp.length) { + media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' '; + if (tmp.attr('value')) { + media += tmp.attr('value'); + } else { + media += '0'; + } + media += '\r\n'; + } + tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]'); + tmp.each(function () { + media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type'); + if ($(this).attr('subtype')) { + media += ' ' + $(this).attr('subtype'); + } + media += '\r\n'; + }); + return media; +}; + +// construct an SDP from a jingle stanza +SDP.prototype.fromJingle = function (jingle) { + var self = this; + this.raw = 'v=0\r\n' + + 'o=- 1923518516 2 IN IP4 0.0.0.0\r\n' +// FIXME + 's=-\r\n' + + 't=0 0\r\n'; + // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8 + if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) { + $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) { + var contents = $(group).find('>content').map(function (idx, content) { + return content.getAttribute('name'); + }).get(); + if (contents.length > 0) { + self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n'; + } + }); + } + + this.session = this.raw; + jingle.find('>content').each(function () { + var m = self.jingle2media($(this)); + self.media.push(m); + }); + + // reconstruct msid-semantic -- apparently not necessary + /* + var msid = SDPUtil.parse_ssrc(this.raw); + if (msid.hasOwnProperty('mslabel')) { + this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n"; + } + */ + + this.raw = this.session + this.media.join(''); +}; + +// translate a jingle content element into an an SDP media part +SDP.prototype.jingle2media = function (content) { + var media = '', + desc = content.find('description'), + ssrc = desc.attr('ssrc'), + self = this, + tmp; + var sctp = content.find( + '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]'); + + tmp = { media: desc.attr('media') }; + tmp.port = '1'; + if (content.attr('senders') == 'rejected') { + // estos hack to reject an m-line. + tmp.port = '0'; + } + if (content.find('>transport>fingerprint').length || desc.find('encryption').length) { + if (sctp.length) + tmp.proto = 'DTLS/SCTP'; + else + tmp.proto = 'RTP/SAVPF'; + } else { + tmp.proto = 'RTP/AVPF'; + } + if (!sctp.length) { + tmp.fmt = desc.find('payload-type').map( + function () { return this.getAttribute('id'); }).get(); + media += SDPUtil.build_mline(tmp) + '\r\n'; + } else { + media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n'; + media += 'a=sctpmap:' + sctp.attr('number') + + ' ' + sctp.attr('protocol'); + + var streamCount = sctp.attr('streams'); + if (streamCount) + media += ' ' + streamCount + '\r\n'; + else + media += '\r\n'; + } + + media += 'c=IN IP4 0.0.0.0\r\n'; + if (!sctp.length) + media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n'; + tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]'); + if (tmp.length) { + if (tmp.attr('ufrag')) { + media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n'; + } + if (tmp.attr('pwd')) { + media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n'; + } + tmp.find('>fingerprint').each(function () { + // FIXME: check namespace at some point + media += 'a=fingerprint:' + this.getAttribute('hash'); + media += ' ' + $(this).text(); + media += '\r\n'; + if (this.getAttribute('setup')) { + media += 'a=setup:' + this.getAttribute('setup') + '\r\n'; + } + }); + } + switch (content.attr('senders')) { + case 'initiator': + media += 'a=sendonly\r\n'; + break; + case 'responder': + media += 'a=recvonly\r\n'; + break; + case 'none': + media += 'a=inactive\r\n'; + break; + case 'both': + media += 'a=sendrecv\r\n'; + break; + } + media += 'a=mid:' + content.attr('name') + '\r\n'; + + // + // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though + // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html + if (desc.find('rtcp-mux').length) { + media += 'a=rtcp-mux\r\n'; + } + + if (desc.find('encryption').length) { + desc.find('encryption>crypto').each(function () { + media += 'a=crypto:' + this.getAttribute('tag'); + media += ' ' + this.getAttribute('crypto-suite'); + media += ' ' + this.getAttribute('key-params'); + if (this.getAttribute('session-params')) { + media += ' ' + this.getAttribute('session-params'); + } + media += '\r\n'; + }); + } + desc.find('payload-type').each(function () { + media += SDPUtil.build_rtpmap(this) + '\r\n'; + if ($(this).find('>parameter').length) { + media += 'a=fmtp:' + this.getAttribute('id') + ' '; + media += $(this).find('parameter').map(function () { + return (this.getAttribute('name') + ? (this.getAttribute('name') + '=') : '') + + this.getAttribute('value'); + }).get().join('; '); + media += '\r\n'; + } + // xep-0293 + media += self.rtcpFbFromJingle($(this), this.getAttribute('id')); + }); + + // xep-0293 + media += self.rtcpFbFromJingle(desc, '*'); + + // xep-0294 + tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]'); + tmp.each(function () { + media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n'; + }); + + content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () { + var protocol = this.getAttribute('protocol'); + protocol = (typeof protocol === 'string') ? protocol.toLowerCase(): ''; + + if ((self.removeTcpCandidates && protocol === 'tcp') || + (self.removeUdpCandidates && protocol === 'udp')) { + return; + } + + media += SDPUtil.candidateFromJingle(this); + }); + + // XEP-0339 handle ssrc-group attributes + content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() { + var semantics = this.getAttribute('semantics'); + var ssrcs = $(this).find('>source').map(function() { + return this.getAttribute('ssrc'); + }).get(); + + if (ssrcs.length) { + media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; + } + }); + + tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); + tmp.each(function () { + var ssrc = this.getAttribute('ssrc'); + $(this).find('>parameter').each(function () { + var name = this.getAttribute('name'); + var value = this.getAttribute('value'); + value = SDPUtil.filter_special_chars(value); + media += 'a=ssrc:' + ssrc + ' ' + name; + if (value && value.length) + media += ':' + value; + media += '\r\n'; + }); + }); + + return media; +}; + + +module.exports = SDP; + + +}).call(this,"/modules/xmpp/SDP.js") +},{"./SDPUtil":30,"jitsi-meet-logger":45}],29:[function(require,module,exports){ +var SDPUtil = require("./SDPUtil"); + +function SDPDiffer(mySDP, otherSDP) +{ + this.mySDP = mySDP; + this.otherSDP = otherSDP; +} + +/** + * Returns map of MediaChannel that contains media contained in + * 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx. + */ +SDPDiffer.prototype.getNewMedia = function() { + + // this could be useful in Array.prototype. + function arrayEquals(array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l=this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) + return false; + } + else if (this[i] != array[i]) { + // Warning - two different object instances will never be + // equal: {x:20} != {x:20} + return false; + } + } + return true; + } + + var myMedias = this.mySDP.getMediaSsrcMap(); + var othersMedias = this.otherSDP.getMediaSsrcMap(); + var newMedia = {}; + Object.keys(othersMedias).forEach(function(othersMediaIdx) { + var myMedia = myMedias[othersMediaIdx]; + var othersMedia = othersMedias[othersMediaIdx]; + if(!myMedia && othersMedia) { + // Add whole channel + newMedia[othersMediaIdx] = othersMedia; + return; + } + // Look for new ssrcs across the channel + Object.keys(othersMedia.ssrcs).forEach(function(ssrc) { + if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) { + // Allocate channel if we've found ssrc that doesn't exist in + // our channel + if(!newMedia[othersMediaIdx]){ + newMedia[othersMediaIdx] = { + mediaindex: othersMedia.mediaindex, + mid: othersMedia.mid, + ssrcs: {}, + ssrcGroups: [] + }; + } + newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc]; + } + }); + + // Look for new ssrc groups across the channels + othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){ + + // try to match the other ssrc-group with an ssrc-group of ours + var matched = false; + for (var i = 0; i < myMedia.ssrcGroups.length; i++) { + var mySsrcGroup = myMedia.ssrcGroups[i]; + if (otherSsrcGroup.semantics == mySsrcGroup.semantics && + arrayEquals.apply(otherSsrcGroup.ssrcs, + [mySsrcGroup.ssrcs])) { + + matched = true; + break; + } + } + + if (!matched) { + // Allocate channel if we've found an ssrc-group that doesn't + // exist in our channel + + if(!newMedia[othersMediaIdx]){ + newMedia[othersMediaIdx] = { + mediaindex: othersMedia.mediaindex, + mid: othersMedia.mid, + ssrcs: {}, + ssrcGroups: [] + }; + } + newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup); + } + }); + }); + return newMedia; +}; + +/** + * TODO: document! + */ +SDPDiffer.prototype.toJingle = function(modify) { + var sdpMediaSsrcs = this.getNewMedia(); + + var modified = false; + Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){ + modified = true; + var media = sdpMediaSsrcs[mediaindex]; + modify.c('content', {name: media.mid}); + + modify.c('description', + {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid}); + // FIXME: not completely sure this operates on blocks and / or handles + // different ssrcs correctly + // generate sources from lines + Object.keys(media.ssrcs).forEach(function(ssrcNum) { + var mediaSsrc = media.ssrcs[ssrcNum]; + modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); + modify.attrs({ssrc: mediaSsrc.ssrc}); + // iterate over ssrc lines + mediaSsrc.lines.forEach(function (line) { + var idx = line.indexOf(' '); + var kv = line.substr(idx + 1); + modify.c('parameter'); + if (kv.indexOf(':') == -1) { + modify.attrs({ name: kv }); + } else { + var nv = kv.split(':', 2); + var name = nv[0]; + var value = SDPUtil.filter_special_chars(nv[1]); + modify.attrs({ name: name }); + modify.attrs({ value: value }); + } + modify.up(); // end of parameter + }); + modify.up(); // end of source + }); + + // generate source groups from lines + media.ssrcGroups.forEach(function(ssrcGroup) { + if (ssrcGroup.ssrcs.length) { + + modify.c('ssrc-group', { + semantics: ssrcGroup.semantics, + xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' + }); + + ssrcGroup.ssrcs.forEach(function (ssrc) { + modify.c('source', { ssrc: ssrc }) + .up(); // end of source + }); + modify.up(); // end of ssrc-group + } + }); + + modify.up(); // end of description + modify.up(); // end of content + }); + + return modified; +}; + +module.exports = SDPDiffer; +},{"./SDPUtil":30}],30:[function(require,module,exports){ +(function (__filename){ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +SDPUtil = { + filter_special_chars: function (text) { + return text.replace(/[\\\/\{,\}\+]/g, ""); + }, + iceparams: function (mediadesc, sessiondesc) { + var data = null; + if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) && + SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) { + data = { + ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)), + pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) + }; + } + return data; + }, + parse_iceufrag: function (line) { + return line.substring(12); + }, + build_iceufrag: function (frag) { + return 'a=ice-ufrag:' + frag; + }, + parse_icepwd: function (line) { + return line.substring(10); + }, + build_icepwd: function (pwd) { + return 'a=ice-pwd:' + pwd; + }, + parse_mid: function (line) { + return line.substring(6); + }, + parse_mline: function (line) { + var parts = line.substring(2).split(' '), + data = {}; + data.media = parts.shift(); + data.port = parts.shift(); + data.proto = parts.shift(); + if (parts[parts.length - 1] === '') { // trailing whitespace + parts.pop(); + } + data.fmt = parts; + return data; + }, + build_mline: function (mline) { + return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' '); + }, + parse_rtpmap: function (line) { + var parts = line.substring(9).split(' '), + data = {}; + data.id = parts.shift(); + parts = parts[0].split('/'); + data.name = parts.shift(); + data.clockrate = parts.shift(); + data.channels = parts.length ? parts.shift() : '1'; + return data; + }, + /** + * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it. + * @param line eg. "a=sctpmap:5000 webrtc-datachannel" + * @returns [SCTP port number, protocol, streams] + */ + parse_sctpmap: function (line) + { + var parts = line.substring(10).split(' '); + var sctpPort = parts[0]; + var protocol = parts[1]; + // Stream count is optional + var streamCount = parts.length > 2 ? parts[2] : null; + return [sctpPort, protocol, streamCount];// SCTP port + }, + build_rtpmap: function (el) { + var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate'); + if (el.getAttribute('channels') && el.getAttribute('channels') != '1') { + line += '/' + el.getAttribute('channels'); + } + return line; + }, + parse_crypto: function (line) { + var parts = line.substring(9).split(' '), + data = {}; + data.tag = parts.shift(); + data['crypto-suite'] = parts.shift(); + data['key-params'] = parts.shift(); + if (parts.length) { + data['session-params'] = parts.join(' '); + } + return data; + }, + parse_fingerprint: function (line) { // RFC 4572 + var parts = line.substring(14).split(' '), + data = {}; + data.hash = parts.shift(); + data.fingerprint = parts.shift(); + // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ? + return data; + }, + parse_fmtp: function (line) { + var parts = line.split(' '), + i, key, value, + data = []; + parts.shift(); + parts = parts.join(' ').split(';'); + for (i = 0; i < parts.length; i++) { + key = parts[i].split('=')[0]; + while (key.length && key[0] == ' ') { + key = key.substring(1); + } + value = parts[i].split('=')[1]; + if (key && value) { + data.push({name: key, value: value}); + } else if (key) { + // rfc 4733 (DTMF) style stuff + data.push({name: '', value: key}); + } + } + return data; + }, + parse_icecandidate: function (line) { + var candidate = {}, + elems = line.split(' '); + candidate.foundation = elems[0].substring(12); + candidate.component = elems[1]; + candidate.protocol = elems[2].toLowerCase(); + candidate.priority = elems[3]; + candidate.ip = elems[4]; + candidate.port = elems[5]; + // elems[6] => "typ" + candidate.type = elems[7]; + candidate.generation = 0; // default value, may be overwritten below + for (var i = 8; i < elems.length; i += 2) { + switch (elems[i]) { + case 'raddr': + candidate['rel-addr'] = elems[i + 1]; + break; + case 'rport': + candidate['rel-port'] = elems[i + 1]; + break; + case 'generation': + candidate.generation = elems[i + 1]; + break; + case 'tcptype': + candidate.tcptype = elems[i + 1]; + break; + default: // TODO + logger.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:') { + logger.log('parseCandidate called with a line that is not a candidate line'); + logger.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') { + logger.log('did not find typ in the right place'); + logger.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 + logger.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; +}).call(this,"/modules/xmpp/SDPUtil.js") +},{"jitsi-meet-logger":45}],31:[function(require,module,exports){ +(function (__filename){ +/* global $ */ +var RTC = require('../RTC/RTC'); + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var RTCBrowserType = require("../RTC/RTCBrowserType.js"); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var SSRCReplacement = require("./LocalSSRCReplacement"); + +function TraceablePeerConnection(ice_config, constraints, session) { + var self = this; + this.session = session; + var RTCPeerConnectionType = null; + if (RTCBrowserType.isFirefox()) { + RTCPeerConnectionType = mozRTCPeerConnection; + } else if (RTCBrowserType.isTemasysPluginUsed()) { + RTCPeerConnectionType = RTCPeerConnection; + } else { + RTCPeerConnectionType = webkitRTCPeerConnection; + } + this.peerconnection = new RTCPeerConnectionType(ice_config, constraints); + this.updateLog = []; + this.stats = {}; + this.statsinterval = null; + this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable + var Interop = require('sdp-interop').Interop; + this.interop = new Interop(); + var Simulcast = require('sdp-simulcast'); + this.simulcast = new Simulcast({numOfLayers: 2, explodeRemoteSimulcast: false}); + + // override as desired + this.trace = function (what, info) { + /*logger.warn('WTRACE', what, info); + if (info && RTCBrowserType.isIExplorer()) { + if (info.length > 1024) { + logger.warn('WTRACE', what, info.substr(1024)); + } + if (info.length > 2048) { + logger.warn('WTRACE', what, info.substr(2048)); + } + }*/ + self.updateLog.push({ + time: new Date(), + type: what, + value: info || "" + }); + }; + this.onicecandidate = null; + this.peerconnection.onicecandidate = function (event) { + // FIXME: this causes stack overflow with Temasys Plugin + if (!RTCBrowserType.isTemasysPluginUsed()) + self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' ')); + if (self.onicecandidate !== null) { + self.onicecandidate(event); + } + }; + this.onaddstream = null; + this.peerconnection.onaddstream = function (event) { + self.trace('onaddstream', event.stream.id); + if (self.onaddstream !== null) { + self.onaddstream(event); + } + }; + this.onremovestream = null; + this.peerconnection.onremovestream = function (event) { + self.trace('onremovestream', event.stream.id); + if (self.onremovestream !== null) { + self.onremovestream(event); + } + }; + this.onsignalingstatechange = null; + this.peerconnection.onsignalingstatechange = function (event) { + self.trace('onsignalingstatechange', self.signalingState); + if (self.onsignalingstatechange !== null) { + self.onsignalingstatechange(event); + } + }; + this.oniceconnectionstatechange = null; + this.peerconnection.oniceconnectionstatechange = function (event) { + self.trace('oniceconnectionstatechange', self.iceConnectionState); + if (self.oniceconnectionstatechange !== null) { + self.oniceconnectionstatechange(event); + } + }; + this.onnegotiationneeded = null; + this.peerconnection.onnegotiationneeded = function (event) { + self.trace('onnegotiationneeded'); + if (self.onnegotiationneeded !== null) { + self.onnegotiationneeded(event); + } + }; + self.ondatachannel = null; + this.peerconnection.ondatachannel = function (event) { + self.trace('ondatachannel', event); + if (self.ondatachannel !== null) { + self.ondatachannel(event); + } + }; + // XXX: do all non-firefox browsers which we support also support this? + if (!RTCBrowserType.isFirefox() && this.maxstats) { + this.statsinterval = window.setInterval(function() { + self.peerconnection.getStats(function(stats) { + var results = stats.result(); + var now = new Date(); + for (var i = 0; i < results.length; ++i) { + results[i].names().forEach(function (name) { + var id = results[i].id + '-' + name; + if (!self.stats[id]) { + self.stats[id] = { + startTime: now, + endTime: now, + values: [], + times: [] + }; + } + self.stats[id].values.push(results[i].stat(name)); + self.stats[id].times.push(now.getTime()); + if (self.stats[id].values.length > self.maxstats) { + self.stats[id].values.shift(); + self.stats[id].times.shift(); + } + self.stats[id].endTime = now; + }); + } + }); + + }, 1000); + } +} + +/** + * Returns a string representation of a SessionDescription object. + */ +var dumpSDP = function(description) { + if (typeof description === 'undefined' || description == null) { + return ''; + } + + return 'type: ' + description.type + '\r\n' + description.sdp; +}; + +/** + * Takes a SessionDescription object and returns a "normalized" version. + * Currently it only takes care of ordering the a=ssrc lines. + */ +var normalizePlanB = function(desc) { + if (typeof desc !== 'object' || desc === null || + typeof desc.sdp !== 'string') { + logger.warn('An empty description was passed as an argument.'); + return desc; + } + + var transform = require('sdp-transform'); + var session = transform.parse(desc.sdp); + + if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && + Array.isArray(session.media)) { + session.media.forEach(function (mLine) { + + // Chrome appears to be picky about the order in which a=ssrc lines + // are listed in an m-line when rtx is enabled (and thus there are + // a=ssrc-group lines with FID semantics). Specifically if we have + // "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before + // the "a=ssrc:S1" lines, SRD fails. + // So, put SSRC which appear as the first SSRC in an FID ssrc-group + // first. + var firstSsrcs = []; + var newSsrcLines = []; + + if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) { + mLine.ssrcGroups.forEach(function (group) { + if (typeof group.semantics !== 'undefined' && + group.semantics === 'FID') { + if (typeof group.ssrcs !== 'undefined') { + firstSsrcs.push(Number(group.ssrcs.split(' ')[0])); + } + } + }); + } + + if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { + var i; + for (i = 0; i Plan B + // transformation. + desc = SSRCReplacement.mungeLocalVideoSSRC(desc); + + this.trace('getLocalDescription::preTransform', dumpSDP(desc)); + + // if we're running on FF, transform to Plan B first. + if (RTCBrowserType.usesUnifiedPlan()) { + desc = this.interop.toPlanB(desc); + this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc)); + } + return desc; + }); + TraceablePeerConnection.prototype.__defineGetter__( + 'remoteDescription', + function() { + var desc = this.peerconnection.remoteDescription; + this.trace('getRemoteDescription::preTransform', dumpSDP(desc)); + + // if we're running on FF, transform to Plan B first. + if (RTCBrowserType.usesUnifiedPlan()) { + desc = this.interop.toPlanB(desc); + this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc)); + } + return desc; + }); +} + +TraceablePeerConnection.prototype.addStream = function (stream) { + this.trace('addStream', stream.id); + try + { + this.peerconnection.addStream(stream); + } + catch (e) + { + logger.error(e); + } +}; + +TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) { + this.trace('removeStream', stream.id); + if(stopStreams) { + RTC.stopMediaStream(stream); + } + + try { + // FF doesn't support this yet. + if (this.peerconnection.removeStream) + this.peerconnection.removeStream(stream); + } catch (e) { + logger.error(e); + } +}; + +TraceablePeerConnection.prototype.createDataChannel = function (label, opts) { + this.trace('createDataChannel', label, opts); + return this.peerconnection.createDataChannel(label, opts); +}; + +TraceablePeerConnection.prototype.setLocalDescription + = function (description, successCallback, failureCallback) { + this.trace('setLocalDescription::preTransform', dumpSDP(description)); + // if we're running on FF, transform to Plan A first. + if (RTCBrowserType.usesUnifiedPlan()) { + description = this.interop.toUnifiedPlan(description); + this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description)); + } + + var self = this; + this.peerconnection.setLocalDescription(description, + function () { + self.trace('setLocalDescriptionOnSuccess'); + successCallback(); + }, + function (err) { + self.trace('setLocalDescriptionOnFailure', err); + failureCallback(err); + } + ); + /* + if (this.statsinterval === null && this.maxstats > 0) { + // start gathering stats + } + */ +}; + +TraceablePeerConnection.prototype.setRemoteDescription + = function (description, successCallback, failureCallback) { + this.trace('setRemoteDescription::preTransform', dumpSDP(description)); + // TODO the focus should squeze or explode the remote simulcast + description = this.simulcast.mungeRemoteDescription(description); + this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description)); + + // if we're running on FF, transform to Plan A first. + if (RTCBrowserType.usesUnifiedPlan()) { + description = this.interop.toUnifiedPlan(description); + this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description)); + } + + if (RTCBrowserType.usesPlanB()) { + description = normalizePlanB(description); + } + + var self = this; + this.peerconnection.setRemoteDescription(description, + function () { + self.trace('setRemoteDescriptionOnSuccess'); + successCallback(); + }, + function (err) { + self.trace('setRemoteDescriptionOnFailure', err); + failureCallback(err); + } + ); + /* + if (this.statsinterval === null && this.maxstats > 0) { + // start gathering stats + } + */ +}; + +TraceablePeerConnection.prototype.close = function () { + this.trace('stop'); + if (this.statsinterval !== null) { + window.clearInterval(this.statsinterval); + this.statsinterval = null; + } + this.peerconnection.close(); +}; + +TraceablePeerConnection.prototype.createOffer + = function (successCallback, failureCallback, constraints) { + var self = this; + this.trace('createOffer', JSON.stringify(constraints, null, ' ')); + this.peerconnection.createOffer( + function (offer) { + self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer)); + // NOTE this is not tested because in meet the focus generates the + // offer. + + // if we're running on FF, transform to Plan B first. + if (RTCBrowserType.usesUnifiedPlan()) { + offer = self.interop.toPlanB(offer); + self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer)); + } + + offer = SSRCReplacement.mungeLocalVideoSSRC(offer); + + if (self.session.room.options.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::preTransform', dumpSDP(answer)); + // if we're running on FF, transform to Plan A first. + if (RTCBrowserType.usesUnifiedPlan()) { + answer = self.interop.toPlanB(answer); + self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer)); + } + + // munge local video SSRC + answer = SSRCReplacement.mungeLocalVideoSSRC(answer); + + if (self.session.room.options.enableSimulcast && self.simulcast.isSupported()) { + answer = self.simulcast.mungeLocalDescription(answer); + self.trace('createAnswerOnSuccess::postTransform (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) { + // TODO: Is this the correct way to handle Opera, Temasys? + if (RTCBrowserType.isFirefox()) { + // ignore for now... + if(!errback) + errback = function () {}; + this.peerconnection.getStats(null, callback, errback); + } else { + this.peerconnection.getStats(callback); + } +}; + +module.exports = TraceablePeerConnection; + + +}).call(this,"/modules/xmpp/TraceablePeerConnection.js") +},{"../../service/xmpp/XMPPEvents":84,"../RTC/RTC":13,"../RTC/RTCBrowserType.js":14,"./LocalSSRCReplacement":27,"jitsi-meet-logger":45,"sdp-interop":63,"sdp-simulcast":66,"sdp-transform":73}],32:[function(require,module,exports){ +(function (__filename){ +/* global $, $iq, APP, config, messageHandler, + roomName, sessionTerminated, Strophe, Util */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var Settings = require("../settings/Settings"); + +var AuthenticationEvents + = require("../../service/authentication/AuthenticationEvents"); + +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; + }; +} + + + + + +function Moderator(roomName, xmpp, emitter) { + this.roomName = roomName; + this.xmppService = xmpp; + this.getNextTimeout = createExpBackoffTimer(1000); + this.getNextErrorTimeout = createExpBackoffTimer(1000); + // External authentication stuff + this.externalAuthEnabled = false; + this.settings = new Settings(roomName); + // 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. + this.sipGatewayEnabled = + this.xmppService.options.hosts.call_control !== undefined; + + this.eventEmitter = emitter; + + this.connection = this.xmppService.connection; + this.focusUserJid; + //FIXME: + // Message listener that talks to POPUP window + function listener(event) { + if (event.data && event.data.sessionId) { + if (event.origin !== window.location.origin) { + logger.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); + } +} + +Moderator.prototype.isExternalAuthEnabled = function () { + return this.externalAuthEnabled; +}; + +Moderator.prototype.isSipGatewayEnabled = function () { + return this.sipGatewayEnabled; +}; + + +Moderator.prototype.onMucMemberLeft = function (jid) { + logger.info("Someone left is it focus ? " + jid); + var resource = Strophe.getResourceFromJid(jid); + if (resource === 'focus' && !this.xmppService.sessionTerminated) { + logger.info( + "Focus has left the room - leaving conference"); + //hangUp(); + // We'd rather reload to have everything re-initialized + //FIXME: show some message before reload + this.eventEmitter.emit(XMPPEvents.FOCUS_LEFT); + } +}; + + +Moderator.prototype.setFocusUserJid = function (focusJid) { + if (!this.focusUserJid) { + this.focusUserJid = focusJid; + logger.info("Focus jid set to: " + this.focusUserJid); + } +}; + + +Moderator.prototype.getFocusUserJid = function () { + return this.focusUserJid; +}; + +Moderator.prototype.getFocusComponent = function () { + // Get focus component address + var focusComponent = this.xmppService.options.hosts.focus; + // If not specified use default: 'focus.domain' + if (!focusComponent) { + focusComponent = 'focus.' + this.xmppService.options.hosts.domain; + } + return focusComponent; +}; + +Moderator.prototype.createConferenceIq = function () { + // Generate create conference IQ + var elem = $iq({to: this.getFocusComponent(), type: 'set'}); + + // Session Id used for authentication + var sessionId = localStorage.getItem('sessionId'); + var machineUID = this.settings.getSettings().uid; + + logger.info( + "Session ID: " + sessionId + " machine UID: " + machineUID); + + elem.c('conference', { + xmlns: 'http://jitsi.org/protocol/focus', + room: this.roomName, + 'machine-uid': machineUID + }); + + if (sessionId) { + elem.attrs({ 'session-id': sessionId}); + } + if (this.xmppService.options.hosts.bridge !== undefined) { + elem.c( + 'property', { + name: 'bridge', + value: this.xmppService.options.hosts.bridge + }).up(); + } + // Tell the focus we have Jigasi configured + if (this.xmppService.options.hosts.call_control !== undefined) { + elem.c( + 'property', { + name: 'call_control', + value: this.xmppService.options.hosts.call_control + }).up(); + } + if (this.xmppService.options.channelLastN !== undefined) { + elem.c( + 'property', { + name: 'channelLastN', + value: this.xmppService.options.channelLastN + }).up(); + } + if (this.xmppService.options.adaptiveLastN !== undefined) { + elem.c( + 'property', { + name: 'adaptiveLastN', + value: this.xmppService.options.adaptiveLastN + }).up(); + } + if (this.xmppService.options.adaptiveSimulcast !== undefined) { + elem.c( + 'property', { + name: 'adaptiveSimulcast', + value: this.xmppService.options.adaptiveSimulcast + }).up(); + } + if (this.xmppService.options.openSctp !== undefined) { + elem.c( + 'property', { + name: 'openSctp', + value: this.xmppService.options.openSctp + }).up(); + } + if (this.xmppService.options.startAudioMuted !== undefined) + { + elem.c( + 'property', { + name: 'startAudioMuted', + value: this.xmppService.options.startAudioMuted + }).up(); + } + if (this.xmppService.options.startVideoMuted !== undefined) + { + elem.c( + 'property', { + name: 'startVideoMuted', + value: this.xmppService.options.startVideoMuted + }).up(); + } + elem.c( + 'property', { + name: 'simulcastMode', + value: 'rewriting' + }).up(); + elem.up(); + return elem; +}; + + +Moderator.prototype.parseSessionId = function (resultIq) { + var sessionId = $(resultIq).find('conference').attr('session-id'); + if (sessionId) { + logger.info('Received sessionId: ' + sessionId); + localStorage.setItem('sessionId', sessionId); + } +}; + +Moderator.prototype.parseConfigOptions = function (resultIq) { + + this.setFocusUserJid( + $(resultIq).find('conference').attr('focusjid')); + + var authenticationEnabled + = $(resultIq).find( + '>conference>property' + + '[name=\'authentication\'][value=\'true\']').length > 0; + + logger.info("Authentication enabled: " + authenticationEnabled); + + this.externalAuthEnabled = $(resultIq).find( + '>conference>property' + + '[name=\'externalAuth\'][value=\'true\']').length > 0; + + console.info( + 'External authentication enabled: ' + this.externalAuthEnabled); + + if (!this.externalAuthEnabled) { + // We expect to receive sessionId in 'internal' authentication mode + this.parseSessionId(resultIq); + } + + var authIdentity = $(resultIq).find('>conference').attr('identity'); + + this.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) { + this.sipGatewayEnabled = true; + } + + logger.info("Sip gateway enabled: " + this.sipGatewayEnabled); +}; + +// FIXME = we need to show the fact that we're waiting for the focus +// to the user(or that focus is not available) +Moderator.prototype.allocateConferenceFocus = function (callback) { + // Try to use focus user JID from the config + this.setFocusUserJid(this.xmppService.options.focusUserJid); + // Send create conference IQ + var iq = this.createConferenceIq(); + var self = this; + this.connection.sendIQ( + iq, + function (result) { + + // Setup config options + self.parseConfigOptions(result); + + if ('true' === $(result).find('conference').attr('ready')) { + // Reset both timers + self.getNextTimeout(true); + self.getNextErrorTimeout(true); + // Exec callback + callback(); + } else { + var waitMs = self.getNextTimeout(); + logger.info("Waiting for the focus... " + waitMs); + // Reset error timeout + self.getNextErrorTimeout(true); + window.setTimeout( + function () { + self.allocateConferenceFocus(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) { + logger.info("Session expired! - removing"); + localStorage.removeItem("sessionId"); + } + if ($(error).find('>error>graceful-shutdown').length) { + self.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(); + } + self.eventEmitter.emit( + XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg); + return; + } + // Not authorized to create new room + if ($(error).find('>error>not-authorized').length) { + logger.warn("Unauthorized to start the conference", error); + var toDomain + = Strophe.getDomainFromJid(error.getAttribute('to')); + if (toDomain !== + this.xmppService.options.hosts.anonymousdomain) { + //FIXME: "is external" should come either from + // the focus or config.js + self.externalAuthEnabled = true; + } + self.eventEmitter.emit( + XMPPEvents.AUTHENTICATION_REQUIRED, + function () { + self.allocateConferenceFocus( + callback); + }); + return; + } + var waitMs = self.getNextErrorTimeout(); + logger.error("Focus error, retry after " + waitMs, error); + // Show message + var focusComponent = self.getFocusComponent(); + var retrySec = waitMs / 1000; + //FIXME: message is duplicated ? + // Do not show in case of session invalid + // which means just a retry + if (!invalidSession) { + self.eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED, + focusComponent, retrySec); + } + // Reset response timeout + self.getNextTimeout(true); + window.setTimeout( + function () { + self.allocateConferenceFocus(callback); + }, waitMs); + } + ); +}; + +Moderator.prototype.getLoginUrl = function (urlCallback) { + var iq = $iq({to: this.getFocusComponent(), type: 'get'}); + iq.c('login-url', { + xmlns: 'http://jitsi.org/protocol/focus', + room: this.roomName, + 'machine-uid': this.settings.getSettings().uid + }); + this.connection.sendIQ( + iq, + function (result) { + var url = $(result).find('login-url').attr('url'); + url = url = decodeURIComponent(url); + if (url) { + logger.info("Got auth url: " + url); + urlCallback(url); + } else { + logger.error( + "Failed to get auth url from the focus", result); + } + }, + function (error) { + logger.error("Get auth url error", error); + } + ); +}; +Moderator.prototype.getPopupLoginUrl = function (urlCallback) { + var iq = $iq({to: this.getFocusComponent(), type: 'get'}); + iq.c('login-url', { + xmlns: 'http://jitsi.org/protocol/focus', + room: this.roomName, + 'machine-uid': this.settings.getSettings().uid, + popup: true + }); + this.connection.sendIQ( + iq, + function (result) { + var url = $(result).find('login-url').attr('url'); + url = url = decodeURIComponent(url); + if (url) { + logger.info("Got POPUP auth url: " + url); + urlCallback(url); + } else { + logger.error( + "Failed to get POPUP auth url from the focus", result); + } + }, + function (error) { + logger.error('Get POPUP auth url error', error); + } + ); +}; + +Moderator.prototype.logout = function (callback) { + var iq = $iq({to: this.getFocusComponent(), type: 'set'}); + var sessionId = localStorage.getItem('sessionId'); + if (!sessionId) { + callback(); + return; + } + iq.c('logout', { + xmlns: 'http://jitsi.org/protocol/focus', + 'session-id': sessionId + }); + this.connection.sendIQ( + iq, + function (result) { + var logoutUrl = $(result).find('logout').attr('logout-url'); + if (logoutUrl) { + logoutUrl = decodeURIComponent(logoutUrl); + } + logger.info("Log out OK, url: " + logoutUrl, result); + localStorage.removeItem('sessionId'); + callback(logoutUrl); + }, + function (error) { + logger.error("Logout error", error); + } + ); +}; + +module.exports = Moderator; + + + + +}).call(this,"/modules/xmpp/moderator.js") +},{"../../service/authentication/AuthenticationEvents":80,"../../service/xmpp/XMPPEvents":84,"../settings/Settings":19,"jitsi-meet-logger":45}],33:[function(require,module,exports){ +(function (__filename){ +/* jshint -W117 */ +/* a simple MUC connection plugin + * can only handle a single MUC room + */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var ChatRoom = require("./ChatRoom"); + +module.exports = function(XMPP) { + Strophe.addConnectionPlugin('emuc', { + connection: null, + rooms: {},//map with the rooms + init: function (conn) { + this.connection = conn; + // add handlers (just once) + this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, null, null); + this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null); + this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null); + this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null); + }, + createRoom: function (jid, password, options) { + var roomJid = Strophe.getBareJidFromJid(jid); + if (this.rooms[roomJid]) { + logger.error("You are already in the room!"); + return; + } + this.rooms[roomJid] = new ChatRoom(this.connection, jid, password, XMPP, options); + return this.rooms[roomJid]; + }, + doLeave: function (jid) { + this.rooms[jid].doLeave(); + delete this.rooms[jid]; + }, + onPresence: function (pres) { + var from = pres.getAttribute('from'); + + // What is this for? A workaround for something? + if (pres.getAttribute('type')) { + return true; + } + + var room = this.rooms[Strophe.getBareJidFromJid(from)]; + if(!room) + return; + + // Parse status. + if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) { + room.createNonAnonymousRoom(); + } + + room.onPresence(pres); + + return true; + }, + onPresenceUnavailable: function (pres) { + var from = pres.getAttribute('from'); + var room = this.rooms[Strophe.getBareJidFromJid(from)]; + if(!room) + return; + + room.onPresenceUnavailable(pres, from); + return true; + }, + onPresenceError: function (pres) { + var from = pres.getAttribute('from'); + var room = this.rooms[Strophe.getBareJidFromJid(from)]; + if(!room) + return; + + room.onPresenceError(pres, from); + return true; + }, + onMessage: function (msg) { + // FIXME: this is a hack. but jingle on muc makes nickchanges hard + var from = msg.getAttribute('from'); + var room = this.rooms[Strophe.getBareJidFromJid(from)]; + if(!room) + return; + + room.onMessage(msg, from); + return true; + }, + + setJingleSession: function (from, session) { + var room = this.rooms[Strophe.getBareJidFromJid(from)]; + if(!room) + return; + + room.setJingleSession(session); + } + }); +}; + + +}).call(this,"/modules/xmpp/strophe.emuc.js") +},{"./ChatRoom":24,"jitsi-meet-logger":45}],34:[function(require,module,exports){ +(function (__filename){ +/* jshint -W117 */ + + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var JingleSession = require("./JingleSessionPC"); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var RTCBrowserType = require("../RTC/RTCBrowserType"); + + +module.exports = function(XMPP, eventEmitter) { + Strophe.addConnectionPlugin('jingle', { + connection: null, + sessions: {}, + jid2session: {}, + ice_config: {iceServers: []}, + media_constraints: { + mandatory: { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': true + } + // MozDontOfferDataChannel: true when this is firefox + }, + init: function (conn) { + this.connection = conn; + if (this.connection.disco) { + // http://xmpp.org/extensions/xep-0167.html#support + // http://xmpp.org/extensions/xep-0176.html#support + this.connection.disco.addFeature('urn:xmpp:jingle:1'); + this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1'); + this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1'); + this.connection.disco.addFeature('urn:xmpp:jingle:apps:dtls:0'); + this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1'); + this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio'); + this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video'); + + if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera() + || RTCBrowserType.isTemasysPluginUsed()) { + this.connection.disco.addFeature('urn:ietf:rfc:4588'); + } + + // this is dealt with by SDP O/A so we don't need to announce this + //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293 + //this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294 + + this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux + this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle + + //this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc + } + this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null); + }, + onJingle: function (iq) { + var sid = $(iq).find('jingle').attr('sid'); + var action = $(iq).find('jingle').attr('action'); + var fromJid = iq.getAttribute('from'); + // send ack first + var ack = $iq({type: 'result', + to: fromJid, + id: iq.getAttribute('id') + }); + logger.log('on jingle ' + action + ' from ' + fromJid, iq); + var sess = this.sessions[sid]; + if ('session-initiate' != action) { + if (sess === null) { + ack.type = 'error'; + ack.c('error', {type: 'cancel'}) + .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up() + .c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'}); + this.connection.send(ack); + return true; + } + // local jid is not checked + if (fromJid != sess.peerjid) { + logger.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(); + logger.warn('duplicate session id', sid); + this.connection.send(ack); + return true; + } + // FIXME: check for a defined action + this.connection.send(ack); + // see http://xmpp.org/extensions/xep-0166.html#concepts-session + switch (action) { + case 'session-initiate': + console.log("(TIME) received session-initiate:\t", + window.performance.now()); + var startMuted = $(iq).find('jingle>startmuted'); + if (startMuted && startMuted.length > 0) { + var audioMuted = startMuted.attr("audio"); + var videoMuted = startMuted.attr("video"); + eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS, + audioMuted === "true", videoMuted === "true"); + } + sess = new JingleSession( + $(iq).attr('to'), $(iq).find('jingle').attr('sid'), + this.connection, XMPP); + // configure session + + var fromBareJid = Strophe.getBareJidFromJid(fromJid); + this.connection.emuc.setJingleSession(fromBareJid, sess); + + sess.media_constraints = this.media_constraints; + sess.ice_config = this.ice_config; + + sess.initialize(fromJid, false); + eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess); + // FIXME: setRemoteDescription should only be done when this call is to be accepted + sess.setOffer($(iq).find('>jingle')); + + this.sessions[sess.sid] = sess; + this.jid2session[sess.peerjid] = sess; + + // the callback should either + // .sendAnswer and .accept + // or .sendTerminate -- not necessarily synchronous + + sess.sendAnswer(); + sess.accept(); + break; + case 'session-accept': + sess.setAnswer($(iq).find('>jingle')); + sess.accept(); + 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; + } + logger.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')); + break; + case 'removesource': // FIXME: proprietary, un-jingleish + case 'source-remove': // FIXME: proprietary + sess.removeSource($(iq).find('>jingle>content')); + break; + default: + logger.warn('jingle action not implemented', action); + break; + } + return true; + }, + 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]; + } + }, + 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) { + logger.warn('getting turn credentials failed', err); + logger.warn('is mod_turncredentials or similar installed?'); + } + ); + // implement push? + }, + + /** + * Returns the data saved in 'updateLog' in a format to be logged. + */ + getLog: function () { + var data = {}; + var self = this; + Object.keys(this.sessions).forEach(function (sid) { + var session = self.sessions[sid]; + if (session.peerconnection && session.peerconnection.updateLog) { + // FIXME: should probably be a .dump call + data["jingle_" + session.sid] = { + updateLog: session.peerconnection.updateLog, + stats: session.peerconnection.stats, + url: window.location.href + }; + } + }); + return data; + } + }); +}; + + +}).call(this,"/modules/xmpp/strophe.jingle.js") +},{"../../service/xmpp/XMPPEvents":84,"../RTC/RTCBrowserType":14,"./JingleSessionPC":26,"jitsi-meet-logger":45}],35:[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]); + } + }); +}; +},{}],36:[function(require,module,exports){ +(function (__filename){ +/* global $, $iq, Strophe */ + +var logger = require("jitsi-meet-logger").getLogger(__filename); + +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); + +/** + * Ping every 20 sec + */ +var PING_INTERVAL = 20000; + +/** + * Ping timeout error after 15 sec of waiting. + */ +var PING_TIMEOUT = 15000; + +/** + * Will close the connection after 3 consecutive ping errors. + */ +var PING_THRESHOLD = 3; + +/** + * XEP-0199 ping plugin. + * + * Registers "urn:xmpp:ping" namespace under Strophe.NS.PING. + */ +module.exports = function (XMPP, eventEmitter) { + Strophe.addConnectionPlugin('ping', { + + connection: null, + + failedPings: 0, + + /** + * Initializes the plugin. Method called by Strophe. + * @param connection Strophe connection instance. + */ + init: function (connection) { + this.connection = connection; + Strophe.addNamespace('PING', "urn:xmpp:ping"); + }, + + /** + * Sends "ping" to given jid + * @param jid the JID to which ping request will be sent. + * @param success callback called on success. + * @param error callback called on error. + * @param timeout ms how long are we going to wait for the response. On + * timeout error callback is called with undefined error + * argument. + */ + ping: function (jid, success, error, timeout) { + var iq = $iq({type: 'get', to: jid}); + iq.c('ping', {xmlns: Strophe.NS.PING}); + this.connection.sendIQ(iq, success, error, timeout); + }, + + /** + * Checks if given jid has XEP-0199 ping support. + * @param jid the JID to be checked for ping support. + * @param callback function with boolean argument which will be + * true if XEP-0199 ping is supported by given jid + */ + hasPingSupport: function (jid, callback) { + this.connection.disco.info( + jid, null, + function (result) { + var ping = $(result).find('>>feature[var="urn:xmpp:ping"]'); + callback(ping.length > 0); + }, + function (error) { + logger.error("Ping feature discovery error", error); + callback(false); + } + ); + }, + + /** + * Starts to send ping in given interval to specified remote JID. + * This plugin supports only one such task and stopInterval + * must be called before starting a new one. + * @param remoteJid remote JID to which ping requests will be sent to. + * @param interval task interval in ms. + */ + startInterval: function (remoteJid, interval) { + if (this.intervalId) { + logger.error("Ping task scheduled already"); + return; + } + if (!interval) + interval = PING_INTERVAL; + var self = this; + this.intervalId = window.setInterval(function () { + self.ping(remoteJid, + function (result) { + // Ping OK + self.failedPings = 0; + }, + function (error) { + self.failedPings += 1; + logger.error( + "Ping " + (error ? "error" : "timeout"), error); + if (self.failedPings >= PING_THRESHOLD) { + self.connection.disconnect(); + } + }, PING_TIMEOUT); + }, interval); + logger.info("XMPP pings will be sent every " + interval + " ms"); + }, + + /** + * Stops current "ping" interval task. + */ + stopInterval: function () { + if (this.intervalId) { + window.clearInterval(this.intervalId); + this.intervalId = null; + this.failedPings = 0; + logger.info("Ping interval cleared"); + } + } + }); +}; + +}).call(this,"/modules/xmpp/strophe.ping.js") +},{"../../service/xmpp/XMPPEvents":84,"jitsi-meet-logger":45}],37:[function(require,module,exports){ +(function (__filename){ +/* jshint -W117 */ +var logger = require("jitsi-meet-logger").getLogger(__filename); + +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) { + logger.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) { + logger.info('Dial result ', result); + + var resource = $(result).find('ref').attr('uri'); + this.call_resource = resource.substr('xmpp:'.length); + logger.info( + "Received call resource: " + this.call_resource); + }, + function (error) { + logger.info('Dial error ', error); + } + ); + }, + hang_up: function () { + if (!this.call_resource) { + logger.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) { + logger.info('Hangup result ', result); + self.call_resource = null; + }, + function (error) { + logger.info('Hangup error ', error); + self.call_resource = null; + } + ); + } + } + ); +}; + +}).call(this,"/modules/xmpp/strophe.rayo.js") +},{"jitsi-meet-logger":45}],38:[function(require,module,exports){ +(function (__filename){ +/** + * Strophe logger implementation. Logs from level WARN and above. + */ +var logger = require("jitsi-meet-logger").getLogger(__filename); + +module.exports = function () { + + Strophe.log = function (level, msg) { + switch (level) { + case Strophe.LogLevel.WARN: + logger.warn("Strophe: " + msg); + break; + case Strophe.LogLevel.ERROR: + case Strophe.LogLevel.FATAL: + logger.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"; + } + }; +}; + +}).call(this,"/modules/xmpp/strophe.util.js") +},{"jitsi-meet-logger":45}],39:[function(require,module,exports){ +(function (__filename){ +/* global $, APP, config, Strophe*/ + +var logger = require("jitsi-meet-logger").getLogger(__filename); +var EventEmitter = require("events"); +var Pako = require("pako"); +var StreamEventTypes = require("../../service/RTC/StreamEventTypes"); +var RTCEvents = require("../../service/RTC/RTCEvents"); +var XMPPEvents = require("../../service/xmpp/XMPPEvents"); +var JitsiConnectionErrors = require("../../JitsiConnectionErrors"); +var JitsiConnectionEvents = require("../../JitsiConnectionEvents"); +var RTC = require("../RTC/RTC"); + +var authenticatedUser = false; + +function createConnection(bosh) { + bosh = bosh || '/http-bind'; + + return new Strophe.Connection(bosh); +}; + + + +//!!!!!!!!!! FIXME: ... +function initStrophePlugins(XMPP) +{ + require("./strophe.emuc")(XMPP); + require("./strophe.jingle")(XMPP, XMPP.eventEmitter); +// require("./strophe.moderate")(XMPP, eventEmitter); + require("./strophe.util")(); + require("./strophe.ping")(XMPP, XMPP.eventEmitter); + require("./strophe.rayo")(); + require("./strophe.logger")(); +} + +//!!!!!!!!!! FIXME: ... +///** +// * If given localStream is video one this method will advertise it's +// * video type in MUC presence. +// * @param localStream new or modified LocalStream. +// */ +//function broadcastLocalVideoType(localStream) { +// if (localStream.videoType) +// XMPP.addToPresence('videoType', localStream.videoType); +//} +// +//function registerListeners() { +// RTC.addStreamListener( +// broadcastLocalVideoType, +// StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED +// ); +// RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) { +// XMPP.addToPresence("devices", devices); +// }); +//} + +function XMPP(options) { + this.eventEmitter = new EventEmitter(); + this.connection = null; + this.disconnectInProgress = false; + + this.forceMuted = false; + this.options = options; + initStrophePlugins(this); +// registerListeners(); + + this.connection = createConnection(options.bosh); +} + + +XMPP.prototype.getConnection = function(){ return connection; }; + +XMPP.prototype._connect = function (jid, password) { + + var self = this; + // 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; + this.connection.connect(jid, password, function (status, msg) { + logger.log("(TIME) Strophe " + Strophe.getStatusString(status) + + (msg ? "[" + msg + "]" : "") + "\t:" + window.performance.now()); + if (status === Strophe.Status.CONNECTED) { + if (self.options.useStunTurn) { + self.connection.jingle.getStunAndTurnCredentials(); + } + + + logger.info("My Jabber ID: " + self.connection.jid); + + // Schedule ping ? + var pingJid = self.connection.domain; + self.connection.ping.hasPingSupport( + pingJid, + function (hasPing) { + if (hasPing) + self.connection.ping.startInterval(pingJid); + else + logger.warn("Ping NOT supported by " + pingJid); + } + ); + + if (password) + authenticatedUser = true; + if (self.connection && self.connection.connected && + Strophe.getResourceFromJid(self.connection.jid)) { + // .connected is true while connecting? +// self.connection.send($pres()); + self.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_ESTABLISHED, + Strophe.getResourceFromJid(self.connection.jid)); + } + } else if (status === Strophe.Status.CONNFAIL) { + if (msg === 'x-strophe-bad-non-anon-jid') { + anonymousConnectionFailed = true; + } + else + { + connectionFailed = true; + } + lastErrorMsg = msg; + } else if (status === Strophe.Status.DISCONNECTED) { + // Stop ping interval + self.connection.ping.stopInterval(); + self.disconnectInProgress = false; + if (anonymousConnectionFailed) { + // prompt user for username and password + self.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED, + JitsiConnectionErrors.PASSWORD_REQUIRED); + } else if(connectionFailed) { + self.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED, + JitsiConnectionErrors.OTHER_ERROR, + msg ? msg : lastErrorMsg); + } else { + self.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_DISCONNECTED, + msg ? msg : lastErrorMsg); + } + } else if (status === Strophe.Status.AUTHFAIL) { + // wrong password or username, prompt user + self.eventEmitter.emit(JitsiConnectionEvents.CONNECTION_FAILED, + JitsiConnectionErrors.PASSWORD_REQUIRED); + + } + }); +} + +XMPP.prototype.connect = function (jid, password) { + if(!jid) { + var configDomain = this.options.hosts.anonymousdomain || this.options.hosts.domain; + // Force authenticated domain if room is appended with '?login=true' + if (this.options.hosts.anonymousdomain && + window.location.search.indexOf("login=true") !== -1) { + configDomain = this.options.hosts.domain; + } + jid = configDomain || window.location.hostname; + } + return this._connect(jid, password); +}; + +XMPP.prototype.createRoom = function (roomName, options, useNicks, nick) { + var roomjid = roomName + '@' + this.options.hosts.muc; + + if (useNicks) { + if (nick) { + roomjid += '/' + nick; + } else { + roomjid += '/' + Strophe.getNodeFromJid(this.connection.jid); + } + } else { + var tmpJid = Strophe.getNodeFromJid(this.connection.jid); + + if(!authenticatedUser) + tmpJid = tmpJid.substr(0, 8); + + roomjid += '/' + tmpJid; + } + + return this.connection.emuc.createRoom(roomjid, null, options); +} + +XMPP.prototype.addListener = function(type, listener) { + this.eventEmitter.on(type, listener); +}; + +XMPP.prototype.removeListener = function (type, listener) { + this.eventEmitter.removeListener(type, listener); +}; + +//FIXME: this should work with the room +XMPP.prototype.leaveRoom = function (jid) { + var handler = this.connection.jingle.jid2session[jid]; + if (handler && handler.peerconnection) { + // FIXME: probably removing streams is not required and close() should + // be enough + if (RTC.localAudio) { + handler.peerconnection.removeStream( + RTC.localAudio.getOriginalStream(), true); + } + if (RTC.localVideo) { + handler.peerconnection.removeStream( + RTC.localVideo.getOriginalStream(), true); + } + handler.peerconnection.close(); + } + this.eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE); + this.connection.emuc.doLeave(jid); +}; + +/** + * 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. + */ +XMPP.prototype.sendLogs = function (data) { + if(!this.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: this.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(); + + this.connection.send(message); + return true; +}; + +// Gets the logs from strophe.jingle. +XMPP.prototype.getJingleLog = function () { + return this.connection.jingle ? this.connection.jingle.getLog() : {}; +}; + +// Gets the logs from strophe. +XMPP.prototype.getXmppLog = function () { + return this.connection.logger ? this.connection.logger.log : null; +}; + + +XMPP.prototype.dial = function (to, from, roomName,roomPass) { + this.connection.rayo.dial(to, from, roomName,roomPass); +}; + +XMPP.prototype.setMute = function (jid, mute) { + this.connection.moderate.setMute(jid, mute); +}; + +XMPP.prototype.eject = function (jid) { + this.connection.moderate.eject(jid); +}; + +XMPP.prototype.getSessions = function () { + return this.connection.jingle.sessions; +}; + +XMPP.prototype.disconnect = function () { + if (this.disconnectInProgress || !this.connection || !this.connection.connected) + { + this.eventEmitter.emit(JitsiConnectionEvents.WRONG_STATE); + return; + } + + this.disconnectInProgress = true; + + this.connection.disconnect(); +}; + + +module.exports = XMPP; + +}).call(this,"/modules/xmpp/xmpp.js") +},{"../../JitsiConnectionErrors":5,"../../JitsiConnectionEvents":6,"../../service/RTC/RTCEvents":77,"../../service/RTC/StreamEventTypes":79,"../../service/xmpp/XMPPEvents":84,"../RTC/RTC":13,"./strophe.emuc":33,"./strophe.jingle":34,"./strophe.logger":35,"./strophe.ping":36,"./strophe.rayo":37,"./strophe.util":38,"events":41,"jitsi-meet-logger":45,"pako":46}],40:[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