Ver código fonte

Merge remote-tracking branch 'remotes/upstream/master' into set_mute

master
tsareg 9 anos atrás
pai
commit
b6ad86ac19

+ 2
- 2
JitsiConference.js Ver arquivo

@@ -98,8 +98,6 @@ JitsiConference.prototype._init = function (options) {
98 98
         this.statistics = new Statistics(this.xmpp, {
99 99
             callStatsID: this.options.config.callStatsID,
100 100
             callStatsSecret: this.options.config.callStatsSecret,
101
-            disableThirdPartyRequests:
102
-                this.options.config.disableThirdPartyRequests,
103 101
             roomName: this.options.name
104 102
         });
105 103
     }
@@ -786,6 +784,8 @@ function (jingleSession, jingleOffer, now) {
786 784
     // Accept incoming call
787 785
     this.room.setJingleSession(jingleSession);
788 786
     this.room.connectionTimes["session.initiate"] = now;
787
+    Statistics.analytics.sendEvent("muc.idle",
788
+        (now - this.room.connectionTimes["muc.joined"]));
789 789
     try{
790 790
         jingleSession.initialize(false /* initiator */,this.room);
791 791
     } catch (error) {

+ 27
- 4
JitsiConferenceEventManager.js Ver arquivo

@@ -92,6 +92,19 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function () {
92 92
 
93 93
     this.chatRoomForwarder.forward(XMPPEvents.MUC_JOINED,
94 94
         JitsiConferenceEvents.CONFERENCE_JOINED);
95
+    // send some analytics events
96
+    chatRoom.addListener(XMPPEvents.MUC_JOINED,
97
+        function ()
98
+        {
99
+            for (var ckey in chatRoom.connectionTimes){
100
+                var cvalue = chatRoom.connectionTimes[ckey];
101
+                Statistics.analytics.sendEvent('conference.' + ckey, cvalue);
102
+            }
103
+            for (var xkey in chatRoom.xmpp.connectionTimes){
104
+                var xvalue = chatRoom.xmpp.connectionTimes[xkey];
105
+                Statistics.analytics.sendEvent('xmpp.' + xkey, xvalue);
106
+            }
107
+        });
95 108
 
96 109
     this.chatRoomForwarder.forward(XMPPEvents.ROOM_JOIN_ERROR,
97 110
         JitsiConferenceEvents.CONFERENCE_FAILED,
@@ -116,6 +129,10 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function () {
116 129
     this.chatRoomForwarder.forward(XMPPEvents.BRIDGE_DOWN,
117 130
         JitsiConferenceEvents.CONFERENCE_FAILED,
118 131
         JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE);
132
+    chatRoom.addListener(XMPPEvents.BRIDGE_DOWN,
133
+        function (){
134
+            Statistics.analytics.sendEvent('conference.bridgeDown');
135
+        });
119 136
 
120 137
     this.chatRoomForwarder.forward(XMPPEvents.RESERVATION_ERROR,
121 138
         JitsiConferenceEvents.CONFERENCE_FAILED,
@@ -153,19 +170,24 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function () {
153 170
 
154 171
     chatRoom.addListener(XMPPEvents.FOCUS_LEFT,
155 172
         function () {
173
+            Statistics.analytics.sendEvent('conference.focusLeft');
156 174
             if(!conference.connection._reload())
157 175
                 conference.eventEmitter.emit(
158 176
                     JitsiConferenceEvents.CONFERENCE_FAILED,
159 177
                     JitsiConferenceErrors.FOCUS_LEFT);
160 178
         });
161 179
 
162
-    chatRoom.addListener(XMPPEvents.ALLOCATE_FOCUS_MAX_RETRIES_ERROR,
163
-        function () {
164
-            conference.connection._reload();
165
-        });
180
+    var reloadHandler = function () { conference.connection._reload(); };
181
+    chatRoom.addListener(
182
+        XMPPEvents.ALLOCATE_FOCUS_MAX_RETRIES_ERROR, reloadHandler);
183
+    chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT, reloadHandler);
166 184
 
167 185
     this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_INTERRUPTED,
168 186
         JitsiConferenceEvents.CONNECTION_INTERRUPTED);
187
+    chatRoom.addListener(XMPPEvents.CONNECTION_INTERRUPTED,
188
+        function () {
189
+            Statistics.analytics.sendEvent('connection.interrupted');
190
+        });
169 191
 
170 192
     this.chatRoomForwarder.forward(XMPPEvents.RECORDER_STATE_CHANGED,
171 193
         JitsiConferenceEvents.RECORDER_STATE_CHANGED);
@@ -428,6 +450,7 @@ JitsiConferenceEventManager.prototype.setupRTCListeners = function () {
428 450
         var now = window.performance.now();
429 451
         logger.log("(TIME) data channel opened ", now);
430 452
         conference.room.connectionTimes["data.channel.opened"] = now;
453
+        Statistics.analytics.sendEvent('conference.dataChannel.open', now);
431 454
     });
432 455
 
433 456
     this.rtcForwarder.forward(RTCEvents.LASTN_CHANGED,

+ 13
- 0
JitsiConnection.js Ver arquivo

@@ -2,6 +2,7 @@ var JitsiConference = require("./JitsiConference");
2 2
 var XMPP = require("./modules/xmpp/xmpp");
3 3
 var JitsiConnectionEvents = require("./JitsiConnectionEvents");
4 4
 var JitsiConnectionErrors = require("./JitsiConnectionErrors");
5
+var Statistics = require("./modules/statistics/statistics");
5 6
 
6 7
 /**
7 8
  * Creates new connection object for the Jitsi Meet server side video conferencing service. Provides access to the
@@ -25,12 +26,23 @@ function JitsiConnection(appID, token, options) {
25 26
 
26 27
     this.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED,
27 28
         function (errType, msg) {
29
+            Statistics.analytics.sendEvent('connection.failed.' + errType);
28 30
             if(errType === JitsiConnectionErrors.OTHER_ERROR &&
29 31
                 (msg === "item-not-found" || msg === "host-unknown")) {
30 32
                     // FIXME: don't report the error if we are going to reload
31 33
                     this._reload();
32 34
                 }
33 35
         }.bind(this));
36
+
37
+    this.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED,
38
+        function (msg) {
39
+            // we can see disconnects from normal tab closing of the browser
40
+            // and then there are no msgs, but we want to log only disconnects
41
+            // when there is real error
42
+            if(msg)
43
+                Statistics.analytics.sendEvent(
44
+                    'connection.disconnected.' + msg);
45
+        });
34 46
 }
35 47
 
36 48
 /**
@@ -62,6 +74,7 @@ JitsiConnection.prototype.attach = function (options) {
62 74
 JitsiConnection.prototype._reload = function () {
63 75
     if(this.retryOnFail === 0)
64 76
         return false;
77
+    Statistics.analytics.sendEvent('connection.reload');
65 78
     this.retryOnFail--;
66 79
     var states = {};
67 80
     for(var name in this.conferences) {

+ 3
- 1
JitsiMediaDevices.js Ver arquivo

@@ -46,7 +46,9 @@ var JitsiMediaDevices = {
46 46
     },
47 47
     /**
48 48
      * Checks if its possible to enumerate available cameras/micropones.
49
-     * @returns {boolean} true if available, false otherwise.
49
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
50
+     * the WebRTC stack is ready, either with true if the device listing is
51
+     * available available or with false otherwise.
50 52
      */
51 53
     isDeviceListAvailable: function () {
52 54
         return RTC.isDeviceListAvailable();

+ 77
- 14
JitsiMeetJS.js Ver arquivo

@@ -41,6 +41,29 @@ function getLowerResolution(resolution) {
41 41
     return resName;
42 42
 }
43 43
 
44
+/**
45
+ * Checks the available devices in options and concatenate the data to the
46
+ * name, which will be used as analytics event name. Adds resolution for the
47
+ * devices.
48
+ * @param name name of event
49
+ * @param options gum options
50
+ * @returns {*}
51
+ */
52
+function addDeviceTypeToAnalyticsEvent(name, options) {
53
+    if (options.devices.indexOf("audio") !== -1) {
54
+        name += ".audio";
55
+    }
56
+    if (options.devices.indexOf("desktop") !== -1) {
57
+        name += ".desktop";
58
+    }
59
+    if (options.devices.indexOf("video") !== -1) {
60
+        // we have video add resolution
61
+        name += ".video." + options.resolution;
62
+    }
63
+
64
+    return name;
65
+}
66
+
44 67
 /**
45 68
  * Namespace for the interface of Jitsi Meet Library.
46 69
  */
@@ -66,13 +89,11 @@ var LibJitsiMeet = {
66 89
     },
67 90
     logLevels: Logger.levels,
68 91
     mediaDevices: JitsiMediaDevices,
92
+    analytics: null,
69 93
     init: function (options) {
70 94
         var logObject, attr;
71
-        Statistics.audioLevelsEnabled = !options.disableAudioLevels;
72
-
73
-        if(typeof options.audioLevelsInterval === 'number') {
74
-            Statistics.audioLevelsInterval = options.audioLevelsInterval;
75
-        }
95
+        Statistics.init(options);
96
+        this.analytics = Statistics.analytics;
76 97
 
77 98
         if (options.enableWindowOnErrorHandler) {
78 99
             GlobalOnErrorHandler.addHandler(
@@ -125,6 +146,28 @@ var LibJitsiMeet = {
125 146
      * will be returned trough the Promise, otherwise JitsiTrack objects will be returned.
126 147
      * @param {string} options.cameraDeviceId
127 148
      * @param {string} options.micDeviceId
149
+     * @param {object} options.desktopSharingExtensionExternalInstallation -
150
+     * enables external installation process for desktop sharing extension if
151
+     * the inline installation is not posible. The following properties should
152
+     * be provided:
153
+     * @param {intiger} interval - the interval (in ms) for
154
+     * checking whether the desktop sharing extension is installed or not
155
+     * @param {Function} checkAgain - returns boolean. While checkAgain()==true
156
+     * createLocalTracks will wait and check on every "interval" ms for the
157
+     * extension. If the desktop extension is not install and checkAgain()==true
158
+     * createLocalTracks will finish with rejected Promise.
159
+     * @param {Function} listener - The listener will be called to notify the
160
+     * user of lib-jitsi-meet that createLocalTracks is starting external
161
+     * extension installation process.
162
+     * NOTE: If the inline installation process is not possible and external
163
+     * installation is enabled the listener property will be called to notify
164
+     * the start of external installation process. After that createLocalTracks
165
+     * will start to check for the extension on every interval ms until the
166
+     * plugin is installed or until checkAgain return false. If the extension
167
+     * is found createLocalTracks will try to get the desktop sharing track and
168
+     * will finish the execution. If checkAgain returns false, createLocalTracks
169
+     * will finish the execution with rejected Promise.
170
+     *
128 171
      * @param {boolean} (firePermissionPromptIsShownEvent) - if event
129 172
      *      JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN should be fired
130 173
      * @returns {Promise.<{Array.<JitsiTrack>}, JitsiConferenceError>}
@@ -137,24 +180,28 @@ var LibJitsiMeet = {
137 180
         if (firePermissionPromptIsShownEvent === true) {
138 181
             window.setTimeout(function () {
139 182
                 if (!promiseFulfilled) {
140
-                    var browser = RTCBrowserType.getBrowserType()
141
-                        .split('rtc_browser.')[1];
142
-
143
-                    if (RTCBrowserType.isAndroid()) {
144
-                        browser = 'android';
145
-                    }
146
-
147 183
                     JitsiMediaDevices.emitEvent(
148 184
                         JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
149
-                        browser);
185
+                        RTCBrowserType.getBrowserName());
150 186
                 }
151 187
             }, USER_MEDIA_PERMISSION_PROMPT_TIMEOUT);
152 188
         }
153 189
 
190
+        if(!window.connectionTimes)
191
+            window.connectionTimes = {};
192
+        window.connectionTimes["obtainPermissions.start"] =
193
+            window.performance.now();
194
+
154 195
         return RTC.obtainAudioAndVideoPermissions(options || {})
155 196
             .then(function(tracks) {
156 197
                 promiseFulfilled = true;
157 198
 
199
+                window.connectionTimes["obtainPermissions.end"] =
200
+                    window.performance.now();
201
+
202
+                Statistics.analytics.sendEvent(addDeviceTypeToAnalyticsEvent(
203
+                    "getUserMedia.success", options), options);
204
+
158 205
                 if(!RTC.options.disableAudioLevels)
159 206
                     for(var i = 0; i < tracks.length; i++) {
160 207
                         var track = tracks[i];
@@ -184,6 +231,9 @@ var LibJitsiMeet = {
184 231
                         logger.debug("Retry createLocalTracks with resolution",
185 232
                             newResolution);
186 233
 
234
+                        Statistics.analytics.sendEvent(
235
+                            "getUserMedia.fail.resolution." + oldResolution);
236
+
187 237
                         return LibJitsiMeet.createLocalTracks(options);
188 238
                     }
189 239
                 }
@@ -198,17 +248,30 @@ var LibJitsiMeet = {
198 248
                         message: error.message
199 249
                     };
200 250
                     Statistics.sendLog(JSON.stringify(logObject));
251
+                    Statistics.analytics.sendEvent(
252
+                        "getUserMedia.userCancel.extensionInstall");
201 253
                 } else {
202 254
                     // Report gUM failed to the stats
203 255
                     Statistics.sendGetUserMediaFailed(error);
204 256
                 }
205 257
 
258
+                window.connectionTimes["obtainPermissions.end"] =
259
+                    window.performance.now();
260
+
261
+
262
+                Statistics.analytics.sendEvent(
263
+                    addDeviceTypeToAnalyticsEvent(
264
+                        "getUserMedia.failed", options) + '.' + error.name,
265
+                    options);
266
+
206 267
                 return Promise.reject(error);
207 268
             }.bind(this));
208 269
     },
209 270
     /**
210 271
      * Checks if its possible to enumerate available cameras/micropones.
211
-     * @returns {boolean} true if available, false otherwise.
272
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
273
+     * the WebRTC stack is ready, either with true if the device listing is
274
+     * available available or with false otherwise.
212 275
      * @deprecated use JitsiMeetJS.mediaDevices.isDeviceListAvailable instead
213 276
      */
214 277
     isDeviceListAvailable: function () {

+ 2
- 2
doc/API.md Ver arquivo

@@ -50,6 +50,8 @@ The ```options``` parameter is JS object with the following properties:
50 50
     9. disableAudioLevels - boolean property. Enables/disables audio levels.
51 51
     10. disableSimulcast - boolean property. Enables/disables simulcast.
52 52
     11. enableWindowOnErrorHandler - boolean property (default false). Enables/disables attaching global onerror handler (window.onerror).
53
+    12. disableThirdPartyRequests - if true - callstats will be disabled and the callstats API won't be included.
54
+    13. analyticsScriptUrl - (optional) custom url to search for the analytics lib, if missing js file will be expected to be next to the library file (the location it is sourced from) 
53 55
 
54 56
 * ```JitsiMeetJS.JitsiConnection``` - the ```JitsiConnection``` constructor. You can use that to create new server connection.
55 57
 
@@ -214,8 +216,6 @@ This objects represents the server connection. You can create new ```JitsiConnec
214 216
         3. jirecon
215 217
         4. callStatsID - callstats credentials
216 218
         5. callStatsSecret - callstats credentials
217
-        6. disableThirdPartyRequests - if true - callstats will be disabled and
218
-        the callstats API won't be included.
219 219
         **NOTE: if 4 and 5 are set the library is going to send events to callstats. Otherwise the callstats integration will be disabled.**
220 220
 
221 221
 5. addEventListener(event, listener) - Subscribes the passed listener to the event.

+ 55
- 9
modules/RTC/JitsiLocalTrack.js Ver arquivo

@@ -7,7 +7,9 @@ var JitsiTrackErrors = require("../../JitsiTrackErrors");
7 7
 var JitsiTrackError = require("../../JitsiTrackError");
8 8
 var RTCEvents = require("../../service/RTC/RTCEvents");
9 9
 var RTCUtils = require("./RTCUtils");
10
+var MediaType = require('../../service/RTC/MediaType');
10 11
 var VideoType = require('../../service/RTC/VideoType');
12
+var CameraFacingMode = require('../../service/RTC/CameraFacingMode');
11 13
 
12 14
 /**
13 15
  * Represents a single media track(either audio or video).
@@ -18,10 +20,11 @@ var VideoType = require('../../service/RTC/VideoType');
18 20
  * @param videoType the VideoType of the JitsiRemoteTrack
19 21
  * @param resolution the video resoultion if it's a video track
20 22
  * @param deviceId the ID of the local device for this track
23
+ * @param facingMode the camera facing mode used in getUserMedia call
21 24
  * @constructor
22 25
  */
23 26
 function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
24
-                         deviceId) {
27
+                         deviceId, facingMode) {
25 28
     var self = this;
26 29
 
27 30
     JitsiTrack.call(this,
@@ -40,6 +43,12 @@ function JitsiLocalTrack(stream, track, mediaType, videoType, resolution,
40 43
     this.initialMSID = this.getMSID();
41 44
     this.inMuteOrUnmuteProgress = false;
42 45
 
46
+    /**
47
+     * The facing mode of the camera from which this JitsiLocalTrack instance
48
+     * was obtained.
49
+     */
50
+    this._facingMode = facingMode;
51
+
43 52
     // Currently there is no way to know the MediaStreamTrack ended due to to
44 53
     // device disconnect in Firefox through e.g. "readyState" property. Instead
45 54
     // we will compare current track's label with device labels from
@@ -204,6 +213,7 @@ JitsiLocalTrack.prototype._setMute = function (mute) {
204 213
             var streamOptions = {
205 214
                 cameraDeviceId: this.getDeviceId(),
206 215
                 devices: ["video"],
216
+                facingMode: this.getCameraFacingMode(),
207 217
                 resolution: this.resolution
208 218
             };
209 219
 
@@ -241,7 +251,7 @@ JitsiLocalTrack.prototype._setMute = function (mute) {
241 251
 
242 252
     return promise
243 253
         .then(function() {
244
-            return self._setConferenceMute(mute);
254
+            return self._sendMuteStatus(mute);
245 255
         })
246 256
         .then(function() {
247 257
             self.eventEmitter.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED);
@@ -302,13 +312,13 @@ JitsiLocalTrack.prototype._removeStreamFromConferenceAsMute = function () {
302 312
 };
303 313
 
304 314
 /**
305
- * Synchronizes muted stated of track with muted state of conference.
315
+ * Sends mute status for a track to conference if any.
306 316
  *
307 317
  * @param {boolean} mute - If track is muted.
308 318
  * @private
309 319
  * @returns {Promise}
310 320
  */
311
-JitsiLocalTrack.prototype._setConferenceMute = function(mute) {
321
+JitsiLocalTrack.prototype._sendMuteStatus = function(mute) {
312 322
     if (!this.conference || !this.conference.room) {
313 323
         return Promise.resolve();
314 324
     }
@@ -316,11 +326,10 @@ JitsiLocalTrack.prototype._setConferenceMute = function(mute) {
316 326
     var self = this;
317 327
 
318 328
     return new Promise(function(resolve) {
319
-        if (self.isAudioTrack()) {
320
-            self.conference.room.setAudioMute(mute, resolve);
321
-        } else {
322
-            self.conference.room.setVideoMute(mute, resolve);
323
-        }
329
+        self.conference.room[
330
+            self.isAudioTrack()
331
+                ? 'setAudioMute'
332
+                : 'setVideoMute'](mute, resolve);
324 333
     });
325 334
 };
326 335
 
@@ -436,4 +445,41 @@ JitsiLocalTrack.prototype.getDeviceId = function () {
436 445
     return this._realDeviceId || this.deviceId;
437 446
 };
438 447
 
448
+/**
449
+ * Returns facing mode for video track from camera. For other cases (e.g. audio
450
+ * track or 'desktop' video track) returns undefined.
451
+ *
452
+ * @returns {CameraFacingMode|undefined}
453
+ */
454
+JitsiLocalTrack.prototype.getCameraFacingMode = function () {
455
+    if (this.isVideoTrack() && this.videoType === VideoType.CAMERA) {
456
+        // MediaStreamTrack#getSettings() is not implemented in many browsers,
457
+        // so we need a feature checking here. Progress on the respective
458
+        // browser's implementation can be tracked at
459
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=2481 for Chromium
460
+        // and https://bugzilla.mozilla.org/show_bug.cgi?id=1213517 for Firefox.
461
+        // Even if a browser implements getSettings() already, it might still
462
+        // not return anything for 'facingMode'.
463
+        var trackSettings;
464
+
465
+        if (this.track &&
466
+            typeof this.track.getSettings === 'function' &&
467
+            (trackSettings = this.track.getSettings()) &&
468
+            'facingMode' in trackSettings) {
469
+            return trackSettings.facingMode;
470
+        } else if (typeof this._facingMode !== 'undefined') {
471
+            return this._facingMode;
472
+        } else {
473
+            // In most cases we are showing a webcam. So if we've gotten here,
474
+            // it should be relatively safe to assume that we are probably
475
+            // showing the user-facing camera.
476
+            return CameraFacingMode.USER;
477
+        }
478
+
479
+    }
480
+
481
+    return undefined;
482
+};
483
+
484
+
439 485
 module.exports = JitsiLocalTrack;

+ 60
- 0
modules/RTC/JitsiRemoteTrack.js Ver arquivo

@@ -1,5 +1,11 @@
1 1
 var JitsiTrack = require("./JitsiTrack");
2 2
 var JitsiTrackEvents = require("../../JitsiTrackEvents");
3
+var RTCBrowserType = require("./RTCBrowserType");
4
+var Statistics = require("../statistics/statistics");
5
+var AdapterJS = require("./adapter.screenshare");
6
+
7
+var ttfmTrackerAudioAttached = false;
8
+var ttfmTrackerVideoAttached = false;
3 9
 
4 10
 /**
5 11
  * Represents a single media track (either audio or video).
@@ -20,6 +26,10 @@ function JitsiRemoteTrack(conference, ownerJid, stream, track, mediaType, videoT
20 26
     this.conference = conference;
21 27
     this.peerjid = ownerJid;
22 28
     this.muted = muted;
29
+    // we want to mark whether the track has been ever muted
30
+    // to detect ttfm events for startmuted conferences, as it can significantly
31
+    // increase ttfm values
32
+    this.hasBeenMuted = muted;
23 33
 }
24 34
 
25 35
 JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype);
@@ -33,6 +43,9 @@ JitsiRemoteTrack.prototype.setMute = function (value) {
33 43
     if(this.muted === value)
34 44
         return;
35 45
 
46
+    if(value)
47
+        this.hasBeenMuted = true;
48
+
36 49
     // we can have a fake video stream
37 50
     if(this.stream)
38 51
         this.stream.muted = value;
@@ -84,4 +97,51 @@ JitsiRemoteTrack.prototype._setVideoType = function (type) {
84 97
     this.eventEmitter.emit(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, type);
85 98
 };
86 99
 
100
+JitsiRemoteTrack.prototype._playCallback = function () {
101
+    var type = (this.isVideoTrack() ? 'video' : 'audio');
102
+
103
+    var now = window.performance.now();
104
+    console.log("(TIME) Render " + type + ":\t", now);
105
+    this.conference.getConnectionTimes()[type + ".render"] = now;
106
+
107
+    var ttfm = now
108
+        - (this.conference.getConnectionTimes()["session.initiate"]
109
+        - this.conference.getConnectionTimes()["muc.joined"])
110
+        - (window.connectionTimes["obtainPermissions.end"]
111
+        - window.connectionTimes["obtainPermissions.start"]);
112
+    this.conference.getConnectionTimes()[type + ".ttfm"] = ttfm;
113
+    console.log("(TIME) TTFM " + type + ":\t", ttfm);
114
+    var eventName = type +'.ttfm';
115
+    if(this.hasBeenMuted)
116
+        eventName += '.muted';
117
+    Statistics.analytics.sendEvent(eventName, ttfm);
118
+};
119
+
120
+/**
121
+ * Attach time to first media tracker only if there is conference and only
122
+ * for the first element.
123
+ * @param container the HTML container which can be 'video' or 'audio' element.
124
+ *        It can also be 'object' element if Temasys plugin is in use and this
125
+ *        method has been called previously on video or audio HTML element.
126
+ * @private
127
+ */
128
+JitsiRemoteTrack.prototype._attachTTFMTracker = function (container) {
129
+    if((ttfmTrackerAudioAttached && this.isAudioTrack())
130
+        || (ttfmTrackerVideoAttached && this.isVideoTrack()))
131
+        return;
132
+
133
+    if (this.isAudioTrack())
134
+        ttfmTrackerAudioAttached = true;
135
+    if (this.isVideoTrack())
136
+        ttfmTrackerVideoAttached = true;
137
+
138
+    if (RTCBrowserType.isTemasysPluginUsed()) {
139
+        // FIXME: this is not working for IE11
140
+        AdapterJS.addEvent(container, 'play', this._playCallback.bind(this));
141
+    }
142
+    else {
143
+        container.addEventListener("canplay", this._playCallback.bind(this));
144
+    }
145
+};
146
+
87 147
 module.exports = JitsiRemoteTrack;

+ 13
- 0
modules/RTC/JitsiTrack.js Ver arquivo

@@ -189,6 +189,8 @@ JitsiTrack.prototype.attach = function (container) {
189 189
 
190 190
     this._maybeFireTrackAttached(container);
191 191
 
192
+    this._attachTTFMTracker(container);
193
+
192 194
     return container;
193 195
 };
194 196
 
@@ -216,6 +218,17 @@ JitsiTrack.prototype.detach = function (container) {
216 218
     }
217 219
 };
218 220
 
221
+/**
222
+ * Attach time to first media tracker only if there is conference and only
223
+ * for the first element.
224
+ * @param container the HTML container which can be 'video' or 'audio' element.
225
+ *        It can also be 'object' element if Temasys plugin is in use and this
226
+ *        method has been called previously on video or audio HTML element.
227
+ * @private
228
+ */
229
+JitsiTrack.prototype._attachTTFMTracker = function (container) {
230
+};
231
+
219 232
 /**
220 233
  * Removes attached event listeners.
221 234
  *

+ 6
- 3
modules/RTC/RTC.js Ver arquivo

@@ -17,16 +17,19 @@ function createLocalTracks(tracksInfo, options) {
17 17
     var deviceId = null;
18 18
     tracksInfo.forEach(function(trackInfo){
19 19
         if (trackInfo.mediaType === MediaType.AUDIO) {
20
-          deviceId = options.micDeviceId;
20
+            deviceId = options.micDeviceId;
21 21
         } else if (trackInfo.videoType === VideoType.CAMERA){
22
-          deviceId = options.cameraDeviceId;
22
+            deviceId = options.cameraDeviceId;
23 23
         }
24 24
         var localTrack
25 25
             = new JitsiLocalTrack(
26 26
                 trackInfo.stream,
27 27
                 trackInfo.track,
28 28
                 trackInfo.mediaType,
29
-                trackInfo.videoType, trackInfo.resolution, deviceId);
29
+                trackInfo.videoType,
30
+                trackInfo.resolution,
31
+                deviceId,
32
+                options.facingMode);
30 33
         newTracks.push(localTrack);
31 34
     });
32 35
     return newTracks;

+ 12
- 0
modules/RTC/RTCBrowserType.js Ver arquivo

@@ -30,6 +30,18 @@ var RTCBrowserType = {
30 30
         return currentBrowser;
31 31
     },
32 32
 
33
+    /**
34
+     * Gets current browser name, split from the type.
35
+     * @returns {string}
36
+     */
37
+    getBrowserName: function () {
38
+        var browser = currentBrowser.split('rtc_browser.')[1];
39
+        if (RTCBrowserType.isAndroid()) {
40
+            browser = 'android';
41
+        }
42
+        return browser;
43
+    },
44
+
33 45
     /**
34 46
      * Checks if current browser is Chrome.
35 47
      * @returns {boolean}

+ 77
- 21
modules/RTC/RTCUtils.js Ver arquivo

@@ -17,6 +17,7 @@ var JitsiTrackErrors = require("../../JitsiTrackErrors");
17 17
 var JitsiTrackError = require("../../JitsiTrackError");
18 18
 var MediaType = require("../../service/RTC/MediaType");
19 19
 var VideoType = require("../../service/RTC/VideoType");
20
+var CameraFacingMode = require("../../service/RTC/CameraFacingMode");
20 21
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
21 22
 
22 23
 var eventEmitter = new EventEmitter();
@@ -42,20 +43,33 @@ var isAudioOutputDeviceChangeAvailable =
42 43
 
43 44
 var currentlyAvailableMediaDevices;
44 45
 
45
-var rawEnumerateDevicesWithCallback = navigator.mediaDevices
46
-    && navigator.mediaDevices.enumerateDevices
46
+var rawEnumerateDevicesWithCallback = undefined;
47
+/**
48
+ * "rawEnumerateDevicesWithCallback" will be initialized only after WebRTC is
49
+ * ready. Otherwise it is too early to assume that the devices listing is not
50
+ * supported.
51
+ */
52
+function initRawEnumerateDevicesWithCallback() {
53
+    rawEnumerateDevicesWithCallback = navigator.mediaDevices
54
+        && navigator.mediaDevices.enumerateDevices
47 55
         ? function(callback) {
48
-            navigator.mediaDevices.enumerateDevices().then(callback, function () {
49
-                callback([]);
56
+            navigator.mediaDevices.enumerateDevices().then(
57
+                callback, function () {
58
+                    callback([]);
50 59
             });
51 60
         }
61
+        // Safari:
62
+        // "ReferenceError: Can't find variable: MediaStreamTrack"
63
+        // when Temasys plugin is not installed yet, have to delay this call
64
+        // until WebRTC is ready.
52 65
         : (MediaStreamTrack && MediaStreamTrack.getSources)
53
-            ? function (callback) {
54
-                MediaStreamTrack.getSources(function (sources) {
55
-                    callback(sources.map(convertMediaStreamTrackSource));
56
-                });
57
-            }
58
-            : undefined;
66
+        ? function (callback) {
67
+            MediaStreamTrack.getSources(function (sources) {
68
+                callback(sources.map(convertMediaStreamTrackSource));
69
+            });
70
+        }
71
+        : undefined;
72
+}
59 73
 
60 74
 // TODO: currently no browser supports 'devicechange' event even in nightly
61 75
 // builds so no feature/browser detection is used at all. However in future this
@@ -103,7 +117,7 @@ function setResolutionConstraints(constraints, resolution) {
103 117
  * @param {string} options.desktopStream
104 118
  * @param {string} options.cameraDeviceId
105 119
  * @param {string} options.micDeviceId
106
- * @param {'user'|'environment'} options.facingMode
120
+ * @param {CameraFacingMode} options.facingMode
107 121
  * @param {bool} firefox_fake_device
108 122
  */
109 123
 function getConstraints(um, options) {
@@ -140,12 +154,13 @@ function getConstraints(um, options) {
140 154
             // TODO: Maybe use "exact" syntax if options.facingMode is defined,
141 155
             // but this probably needs to be decided when updating other
142 156
             // constraints, as we currently don't use "exact" syntax anywhere.
157
+            var facingMode = options.facingMode || CameraFacingMode.USER;
158
+
143 159
             if (isNewStyleConstraintsSupported) {
144
-                constraints.video.facingMode = options.facingMode || 'user';
160
+                constraints.video.facingMode = facingMode;
145 161
             }
146
-
147 162
             constraints.video.optional.push({
148
-                facingMode: options.facingMode || 'user'
163
+                facingMode: facingMode
149 164
             });
150 165
         }
151 166
 
@@ -386,6 +401,9 @@ function onReady (options, GUM) {
386 401
     eventEmitter.emit(RTCEvents.RTC_READY, true);
387 402
     screenObtainer.init(options, GUM);
388 403
 
404
+    // Initialize rawEnumerateDevicesWithCallback
405
+    initRawEnumerateDevicesWithCallback();
406
+
389 407
     if (RTCUtils.isDeviceListAvailable() && rawEnumerateDevicesWithCallback) {
390 408
         rawEnumerateDevicesWithCallback(function (devices) {
391 409
             currentlyAvailableMediaDevices = devices.splice(0);
@@ -925,6 +943,7 @@ var RTCUtils = {
925 943
         var self = this;
926 944
 
927 945
         options = options || {};
946
+        var dsOptions = options.desktopSharingExtensionExternalInstallation;
928 947
         return new Promise(function (resolve, reject) {
929 948
             var successCallback = function (stream) {
930 949
                 resolve(handleLocalStream(stream, options.resolution));
@@ -955,7 +974,8 @@ var RTCUtils = {
955 974
 
956 975
                 if(screenObtainer.isSupported()){
957 976
                     deviceGUM["desktop"] = screenObtainer.obtainStream.bind(
958
-                        screenObtainer);
977
+                        screenObtainer,
978
+                        dsOptions);
959 979
                 }
960 980
                 // With FF/IE we can't split the stream into audio and video because FF
961 981
                 // doesn't support media stream constructors. So, we need to get the
@@ -1016,6 +1036,7 @@ var RTCUtils = {
1016 1036
                             }
1017 1037
                             if(hasDesktop) {
1018 1038
                                 screenObtainer.obtainStream(
1039
+                                    dsOptions,
1019 1040
                                     function (desktopStream) {
1020 1041
                                         successCallback({audioVideo: stream,
1021 1042
                                             desktopStream: desktopStream});
@@ -1034,6 +1055,7 @@ var RTCUtils = {
1034 1055
                         options);
1035 1056
                 } else if (hasDesktop) {
1036 1057
                     screenObtainer.obtainStream(
1058
+                        dsOptions,
1037 1059
                         function (stream) {
1038 1060
                             successCallback({desktopStream: stream});
1039 1061
                         }, function (error) {
@@ -1055,17 +1077,51 @@ var RTCUtils = {
1055 1077
     isRTCReady: function () {
1056 1078
         return rtcReady;
1057 1079
     },
1058
-    /**
1059
-     * Checks if its possible to enumerate available cameras/micropones.
1060
-     * @returns {boolean} true if available, false otherwise.
1061
-     */
1062
-    isDeviceListAvailable: function () {
1080
+    _isDeviceListAvailable: function () {
1081
+        if (!rtcReady)
1082
+            throw new Error("WebRTC not ready yet");
1063 1083
         var isEnumerateDevicesAvailable
1064 1084
             = navigator.mediaDevices && navigator.mediaDevices.enumerateDevices;
1065 1085
         if (isEnumerateDevicesAvailable) {
1066 1086
             return true;
1067 1087
         }
1068
-        return (MediaStreamTrack && MediaStreamTrack.getSources)? true : false;
1088
+        return (typeof MediaStreamTrack !== "undefined" &&
1089
+            MediaStreamTrack.getSources)? true : false;
1090
+    },
1091
+    /**
1092
+     * Returns a promise which can be used to make sure that the WebRTC stack
1093
+     * has been initialized.
1094
+     *
1095
+     * @returns {Promise} which is resolved only if the WebRTC stack is ready.
1096
+     * Note that currently we do not detect stack initialization failure and
1097
+     * the promise is never rejected(unless unexpected error occurs).
1098
+     */
1099
+    onRTCReady: function() {
1100
+        if (rtcReady) {
1101
+            return Promise.resolve();
1102
+        } else {
1103
+            return new Promise(function (resolve) {
1104
+                var listener = function () {
1105
+                    eventEmitter.removeListener(RTCEvents.RTC_READY, listener);
1106
+                    resolve();
1107
+                };
1108
+                eventEmitter.addListener(RTCEvents.RTC_READY, listener);
1109
+                // We have no failed event, so... it either resolves or nothing
1110
+                // happens
1111
+            });
1112
+        }
1113
+    },
1114
+    /**
1115
+     * Checks if its possible to enumerate available cameras/microphones.
1116
+     *
1117
+     * @returns {Promise<boolean>} a Promise which will be resolved only once
1118
+     * the WebRTC stack is ready, either with true if the device listing is
1119
+     * available available or with false otherwise.
1120
+     */
1121
+    isDeviceListAvailable: function () {
1122
+        return this.onRTCReady().then(function() {
1123
+            return this._isDeviceListAvailable();
1124
+        }.bind(this));
1069 1125
     },
1070 1126
     /**
1071 1127
      * Returns true if changing the input (camera / microphone) or output

+ 66
- 15
modules/RTC/ScreenObtainer.js Ver arquivo

@@ -36,6 +36,19 @@ var reDetectFirefoxExtension = false;
36 36
 
37 37
 var GUM = null;
38 38
 
39
+/**
40
+ * The error returned by chrome when trying to start inline installation from
41
+ * popup.
42
+ */
43
+var CHROME_EXTENSION_POPUP_ERROR =
44
+    "Inline installs can not be initiated from pop-up windows.";
45
+
46
+/**
47
+ * The error message returned by chrome when the extension is installed.
48
+ */
49
+var CHROME_NO_EXTENSION_ERROR_MSG =
50
+    "Could not establish connection. Receiving end does not exist.";
51
+
39 52
 /**
40 53
  * Handles obtaining a stream from a screen capture on different browsers.
41 54
  */
@@ -52,6 +65,8 @@ var ScreenObtainer = {
52 65
      * or disable screen capture (if the value is other).
53 66
      * Note that for the "screen" media source to work the
54 67
      * 'chrome://flags/#enable-usermedia-screen-capture' flag must be set.
68
+     * @param options {object}
69
+     * @param gum {Function} GUM method
55 70
      */
56 71
     init: function(options, gum) {
57 72
         var obtainDesktopStream = null;
@@ -66,7 +81,7 @@ var ScreenObtainer = {
66 81
             (options.desktopSharingChromeMethod || options.desktopSharing);
67 82
 
68 83
         if (RTCBrowserType.isNWJS()) {
69
-            obtainDesktopStream = function (onSuccess, onFailure) {
84
+            obtainDesktopStream = function (options, onSuccess, onFailure) {
70 85
                 window.JitsiMeetNW.obtainDesktopStream (
71 86
                     onSuccess, function (error, constraints) {
72 87
                         onFailure && onFailure(new JitsiTrackError(
@@ -133,7 +148,7 @@ var ScreenObtainer = {
133 148
      * @param errorCallback
134 149
      */
135 150
     obtainScreenOnFirefox:
136
-           function (callback, errorCallback) {
151
+           function (options, callback, errorCallback) {
137 152
         var self = this;
138 153
         var extensionRequired = false;
139 154
         if (this.options.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
@@ -146,7 +161,7 @@ var ScreenObtainer = {
146 161
         }
147 162
 
148 163
         if (!extensionRequired || firefoxExtInstalled === true) {
149
-            obtainWebRTCScreen(callback, errorCallback);
164
+            obtainWebRTCScreen(options, callback, errorCallback);
150 165
             return;
151 166
         }
152 167
 
@@ -186,7 +201,7 @@ var ScreenObtainer = {
186 201
      * Asks Chrome extension to call chooseDesktopMedia and gets chrome
187 202
      * 'desktop' stream for returned stream token.
188 203
      */
189
-    obtainScreenFromExtension: function (streamCallback, failCallback) {
204
+    obtainScreenFromExtension: function(options, streamCallback, failCallback) {
190 205
         var self = this;
191 206
         if (chromeExtInstalled) {
192 207
             doGetStreamFromExtension(this.options, streamCallback,
@@ -211,24 +226,60 @@ var ScreenObtainer = {
211 226
                                 streamCallback, failCallback);
212 227
                         }, 500);
213 228
                     },
214
-                    handleExtensionInstallationError
229
+                    this.handleExtensionInstallationError.bind(this,
230
+                        options, streamCallback, failCallback)
215 231
                 );
216 232
             } catch(e) {
217
-                handleExtensionInstallationError(e);
233
+                this.handleExtensionInstallationError(options, streamCallback,
234
+                    failCallback, e);
218 235
             }
219 236
         }
237
+    },
238
+    handleExtensionInstallationError: function (options, streamCallback,
239
+        failCallback, e) {
240
+        if( CHROME_EXTENSION_POPUP_ERROR === e && options.interval > 0 &&
241
+            typeof(options.checkAgain) === "function" &&
242
+            typeof(options.listener) === "function") {
243
+            options.listener(getWebStoreInstallUrl(this.options));
244
+            this.checkForChromeExtensionOnInterval(options,
245
+                streamCallback, failCallback, e);
246
+            return;
247
+        }
248
+        var msg = "Failed to install the extension from "
249
+            + getWebStoreInstallUrl(this.options);
220 250
 
221
-        function handleExtensionInstallationError(e) {
222
-            var msg = "Failed to install the extension from "
223
-                + getWebStoreInstallUrl(self.options);
224
-
225
-            logger.log(msg, e);
251
+        logger.log(msg, e);
226 252
 
253
+        failCallback(new JitsiTrackError(
254
+            JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR,
255
+            msg
256
+        ));
257
+    },
258
+    checkForChromeExtensionOnInterval: function (options,
259
+        streamCallback, failCallback, e) {
260
+        if ( CHROME_EXTENSION_POPUP_ERROR !== e &&
261
+            (e.name !== JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR ||
262
+            !e.message ||
263
+            e.message.message !== CHROME_NO_EXTENSION_ERROR_MSG)) {
264
+            this.handleExtensionInstallationError(null, streamCallback,
265
+                failCallback, e);
266
+            return;
267
+        }
268
+        if (options.checkAgain() === false) {
227 269
             failCallback(new JitsiTrackError(
228
-                JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR,
229
-                msg
230
-            ));
270
+                JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR));
271
+            return;
231 272
         }
273
+        var args = arguments;
274
+        var self = this;
275
+        window.setTimeout(function () {
276
+            doGetStreamFromExtension(self.options,
277
+                function () {
278
+                    chromeExtInstalled = true;
279
+                    streamCallback.apply(null, arguments);
280
+                }, self.checkForChromeExtensionOnInterval.bind(
281
+                    self, options, streamCallback, failCallback));
282
+        }, options.interval);
232 283
     }
233 284
 };
234 285
 
@@ -243,7 +294,7 @@ var ScreenObtainer = {
243 294
  * 'media.getusermedia.screensharing.allowed_domains' preference in
244 295
  * 'about:config'.
245 296
  */
246
-function obtainWebRTCScreen(streamCallback, failCallback) {
297
+function obtainWebRTCScreen(options, streamCallback, failCallback) {
247 298
     GUM(
248 299
         ['screen'],
249 300
         streamCallback,

+ 27
- 0
modules/statistics/AnalyticsAdapter.js Ver arquivo

@@ -0,0 +1,27 @@
1
+var RTCBrowserType = require("../RTC/RTCBrowserType");
2
+
3
+function NoopAnalytics() {}
4
+NoopAnalytics.prototype.sendEvent = function () {};
5
+
6
+function AnalyticsAdapter() {
7
+    this.browserActionSuffix = '.' + RTCBrowserType.getBrowserName();
8
+}
9
+
10
+// XXX Since we asynchronously load the integration of the analytics API and the
11
+// analytics API may asynchronously load its implementation (e.g. Google
12
+// Analytics), we cannot make the decision with respect to which analytics
13
+// implementation we will use here and we have to postpone it i.e. we will make
14
+// a lazy decision.
15
+AnalyticsAdapter.prototype.sendEvent = function (action, data)
16
+{
17
+    if (this.analytics === null || typeof this.analytics === 'undefined') {
18
+        var AnalyticsImpl = window.Analytics || NoopAnalytics;
19
+
20
+        this.analytics = new AnalyticsImpl();
21
+    }
22
+    try {
23
+        this.analytics.sendEvent(action + this.browserActionSuffix, data);
24
+    } catch (ignored) {}
25
+};
26
+
27
+module.exports = new AnalyticsAdapter();

+ 54
- 6
modules/statistics/CallStats.js Ver arquivo

@@ -50,11 +50,13 @@ var callStats = null;
50 50
 function initCallback (err, msg) {
51 51
     logger.log("CallStats Status: err=" + err + " msg=" + msg);
52 52
 
53
+    CallStats.initializeInProgress = false;
54
+
53 55
     // there is no lib, nothing to report to
54
-    if (err !== 'success')
56
+    if (err !== 'success') {
57
+        CallStats.initializeFailed = true;
55 58
         return;
56
-
57
-    CallStats.initialized = true;
59
+    }
58 60
 
59 61
     var ret = callStats.addNewFabric(this.peerconnection,
60 62
         Strophe.getResourceFromJid(this.session.peerjid),
@@ -64,8 +66,14 @@ function initCallback (err, msg) {
64 66
 
65 67
     var fabricInitialized = (ret.status === 'success');
66 68
 
67
-    if(!fabricInitialized)
69
+    if(!fabricInitialized) {
70
+        CallStats.initializeFailed = true;
68 71
         console.log("callstats fabric not initilized", ret.message);
72
+        return;
73
+    }
74
+
75
+    CallStats.initializeFailed = false;
76
+    CallStats.initialized = true;
69 77
 
70 78
     // notify callstats about failures if there were any
71 79
     if (CallStats.reportsQueue.length) {
@@ -145,9 +153,13 @@ var CallStats = _try_catch(function(jingleSession, Settings, options) {
145 153
         // The confID is case sensitive!!!
146 154
         this.confID = location.hostname + "/" + options.roomName;
147 155
 
156
+        this.callStatsID = options.callStatsID;
157
+        this.callStatsSecret = options.callStatsSecret;
158
+
159
+        CallStats.initializeInProgress = true;
148 160
         //userID is generated or given by the origin server
149
-        callStats.initialize(options.callStatsID,
150
-            options.callStatsSecret,
161
+        callStats.initialize(this.callStatsID,
162
+            this.callStatsSecret,
151 163
             this.userID,
152 164
             initCallback.bind(this));
153 165
 
@@ -173,6 +185,37 @@ CallStats.reportsQueue = [];
173 185
  */
174 186
 CallStats.initialized = false;
175 187
 
188
+/**
189
+ * Whether we are in progress of initializing.
190
+ * @type {boolean}
191
+ */
192
+CallStats.initializeInProgress = false;
193
+
194
+/**
195
+ * Whether we tried to initialize and it failed.
196
+ * @type {boolean}
197
+ */
198
+CallStats.initializeFailed = false;
199
+
200
+/**
201
+ * Checks whether we need to re-initialize callstats and starts the process.
202
+ * @private
203
+ */
204
+CallStats._checkInitialize = function () {
205
+    if (CallStats.initialized || !CallStats.initializeFailed
206
+        || !callStats || CallStats.initializeInProgress)
207
+        return;
208
+
209
+    // callstats object created, not initialized and it had previously failed,
210
+    // and there is no init in progress, so lets try initialize it again
211
+    CallStats.initializeInProgress = true;
212
+    callStats.initialize(
213
+        callStats.callStatsID,
214
+        callStats.callStatsSecret,
215
+        callStats.userID,
216
+        initCallback.bind(callStats));
217
+};
218
+
176 219
 /**
177 220
  * Type of pending reports, can be event or an error.
178 221
  * @type {{ERROR: string, EVENT: string}}
@@ -243,6 +286,7 @@ function (ssrc, isLocal, usageLabel, containerId) {
243 286
                     containerId: containerId
244 287
                 }
245 288
             });
289
+            CallStats._checkInitialize();
246 290
         }
247 291
     }).bind(this)();
248 292
 };
@@ -316,6 +360,7 @@ CallStats._reportEvent = function (event, eventData) {
316 360
                 type: reportType.EVENT,
317 361
                 data: {event: event, eventData: eventData}
318 362
             });
363
+        CallStats._checkInitialize();
319 364
     }
320 365
 };
321 366
 
@@ -393,6 +438,7 @@ CallStats._reportError = function (type, e, pc) {
393 438
             type: reportType.ERROR,
394 439
             data: { type: type, error: e, pc: pc}
395 440
         });
441
+        CallStats._checkInitialize();
396 442
     }
397 443
     // else just ignore it
398 444
 };
@@ -479,6 +525,8 @@ CallStats.sendApplicationLog = _try_catch(function (e, cs) {
479 525
 CallStats.dispose = function () {
480 526
     callStats = null;
481 527
     CallStats.initialized = false;
528
+    CallStats.initializeFailed = false;
529
+    CallStats.initializeInProgress = false;
482 530
 };
483 531
 
484 532
 module.exports = CallStats;

+ 39
- 1
modules/statistics/statistics.js Ver arquivo

@@ -4,6 +4,7 @@ var logger = require("jitsi-meet-logger").getLogger(__filename);
4 4
 var RTPStats = require("./RTPStatsCollector.js");
5 5
 var EventEmitter = require("events");
6 6
 var StatisticsEvents = require("../../service/statistics/Events");
7
+var AnalyticsAdapter = require("./AnalyticsAdapter");
7 8
 var CallStats = require("./CallStats");
8 9
 var ScriptUtil = require('../util/ScriptUtil');
9 10
 var JitsiTrackError = require("../../JitsiTrackError");
@@ -32,6 +33,24 @@ function loadCallStatsAPI() {
32 33
     // have loaded by the time we needed it (i.e. CallStats.init is invoked).
33 34
 }
34 35
 
36
+// Load the integration of a third-party analytics API such as Google Analytics.
37
+// Since we cannot guarantee the quality of the third-party service (e.g. their
38
+// server may take noticeably long time to respond), it is in our best interest
39
+// (in the sense that the intergration of the analytics API is important to us
40
+// but not enough to allow it to prevent people from joining a conference) to
41
+// download the API asynchronously. Additionally, Google Analytics will download
42
+// its implementation asynchronously anyway so it makes sense to append the
43
+// loading on our side rather than prepend it.
44
+function loadAnalytics(customScriptUrl) {
45
+    // if we have a custom script url passed as parameter we don't want to
46
+    // search it relatively near the library
47
+    ScriptUtil.loadScript(
48
+        customScriptUrl ? customScriptUrl : 'analytics.js',
49
+        /* async */ true,
50
+        /* prepend */ false,
51
+        /* relativeURL */ customScriptUrl ? false : true);
52
+}
53
+
35 54
 /**
36 55
  * Log stats via the focus once every this many milliseconds.
37 56
  */
@@ -66,6 +85,23 @@ function formatJitsiTrackErrorForCallStats(error) {
66 85
     return err;
67 86
 }
68 87
 
88
+/**
89
+ * Init statistic options
90
+ * @param options
91
+ */
92
+Statistics.init = function (options) {
93
+    Statistics.audioLevelsEnabled = !options.disableAudioLevels;
94
+
95
+    if(typeof options.audioLevelsInterval === 'number') {
96
+        Statistics.audioLevelsInterval = options.audioLevelsInterval;
97
+    }
98
+
99
+    Statistics.disableThirdPartyRequests = options.disableThirdPartyRequests;
100
+
101
+    if (Statistics.disableThirdPartyRequests !== true)
102
+        loadAnalytics(options.analyticsScriptUrl);
103
+}
104
+
69 105
 function Statistics(xmpp, options) {
70 106
     this.rtpStats = null;
71 107
     this.eventEmitter = new EventEmitter();
@@ -76,7 +112,7 @@ function Statistics(xmpp, options) {
76 112
             // Even though AppID and AppSecret may be specified, the integration
77 113
             // of callstats.io may be disabled because of globally-disallowed
78 114
             // requests to any third parties.
79
-            && (this.options.disableThirdPartyRequests !== true);
115
+            && (Statistics.disableThirdPartyRequests !== true);
80 116
     if(this.callStatsIntegrationEnabled)
81 117
         loadCallStatsAPI();
82 118
     this.callStats = null;
@@ -88,6 +124,8 @@ function Statistics(xmpp, options) {
88 124
 }
89 125
 Statistics.audioLevelsEnabled = false;
90 126
 Statistics.audioLevelsInterval = 200;
127
+Statistics.disableThirdPartyRequests = false;
128
+Statistics.analytics = AnalyticsAdapter;
91 129
 
92 130
 /**
93 131
  * Array of callstats instances. Used to call Statistics static methods and

+ 22
- 2
modules/util/ScriptUtil.js Ver arquivo

@@ -1,3 +1,6 @@
1
+var currentExecutingScript = require("current-executing-script");
2
+
3
+
1 4
 /**
2 5
  * Implements utility functions which facilitate the dealing with scripts such
3 6
  * as the download and execution of a JavaScript file.
@@ -12,21 +15,38 @@ var ScriptUtil = {
12 15
      * @param prepend true to schedule the loading of the script as soon as
13 16
      * possible or false to schedule the loading of the script at the end of the
14 17
      * scripts known at the time
18
+     * @param relativeURL whether we need load the library from url relative
19
+     * to the url that lib-jitsi-meet was loaded. Useful when sourcing the
20
+     * library from different location than the app that is using it
15 21
      */
16
-    loadScript: function (src, async, prepend) {
22
+    loadScript: function (src, async, prepend, relativeURL) {
17 23
         var d = document;
18 24
         var tagName = 'script';
19 25
         var script = d.createElement(tagName);
20 26
         var referenceNode = d.getElementsByTagName(tagName)[0];
21 27
 
22 28
         script.async = async;
29
+
30
+        if (relativeURL) {
31
+            // finds the src url of the current loaded script
32
+            // and use it as base of the src supplied argument
33
+            var scriptEl = currentExecutingScript();
34
+            if(scriptEl) {
35
+                var scriptSrc = scriptEl.src;
36
+                var baseScriptSrc
37
+                    = scriptSrc.substring(0, scriptSrc.lastIndexOf('/') + 1);
38
+                if (scriptSrc && baseScriptSrc)
39
+                    src = baseScriptSrc + src;
40
+            }
41
+        }
42
+
23 43
         script.src = src;
24 44
         if (prepend) {
25 45
             referenceNode.parentNode.insertBefore(script, referenceNode);
26 46
         } else {
27 47
             referenceNode.parentNode.appendChild(script);
28 48
         }
29
-    },
49
+    }
30 50
 };
31 51
 
32 52
 module.exports = ScriptUtil;

+ 25
- 3
modules/xmpp/JingleSessionPC.js Ver arquivo

@@ -12,6 +12,7 @@ var XMPPEvents = require("../../service/xmpp/XMPPEvents");
12 12
 var RTCBrowserType = require("../RTC/RTCBrowserType");
13 13
 var RTC = require("../RTC/RTC");
14 14
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
15
+var Statistics = require("../statistics/statistics");
15 16
 
16 17
 /**
17 18
  * Constant tells how long we're going to wait for IQ response, before timeout
@@ -132,6 +133,8 @@ JingleSessionPC.prototype.doInitialize = function () {
132 133
             self.peerconnection.iceConnectionState] = now;
133 134
         logger.log("(TIME) ICE " + self.peerconnection.iceConnectionState +
134 135
                     ":\t", now);
136
+        Statistics.analytics.sendEvent(
137
+            'ice.' + self.peerconnection.iceConnectionState, now);
135 138
         switch (self.peerconnection.iceConnectionState) {
136 139
             case 'connected':
137 140
 
@@ -432,15 +435,34 @@ JingleSessionPC.prototype.sendSessionAccept = function (localSDP,
432 435
     // Calling tree() to print something useful
433 436
     accept = accept.tree();
434 437
     logger.info("Sending session-accept", accept);
435
-
438
+    var self = this;
436 439
     this.connection.sendIQ(accept,
437 440
         success,
438
-        this.newJingleErrorHandler(accept, failure),
441
+        this.newJingleErrorHandler(accept, function (error) {
442
+            failure(error);
443
+            // 'session-accept' is a critical timeout and we'll have to restart
444
+            self.room.eventEmitter.emit(XMPPEvents.SESSION_ACCEPT_TIMEOUT);
445
+        }),
439 446
         IQ_TIMEOUT);
440 447
     // XXX Videobridge needs WebRTC's answer (ICE ufrag and pwd, DTLS
441 448
     // fingerprint and setup) ASAP in order to start the connection
442 449
     // establishment.
443
-    this.connection.flush();
450
+    //
451
+    // FIXME Flushing the connection at this point triggers an issue with BOSH
452
+    // request handling in Prosody on slow connections.
453
+    //
454
+    // The problem is that this request will be quite large and it may take time
455
+    // before it reaches Prosody. In the meantime Strophe may decide to send
456
+    // the next one. And it was observed that a small request with
457
+    // 'transport-info' usually follows this one. It does reach Prosody before
458
+    // the previous one was completely received. 'rid' on the server is
459
+    // increased and Prosody ignores the request with 'session-accept'. It will
460
+    // never reach Jicofo and everything in the request table is lost. Removing
461
+    // the flush does not guarantee it will never happen, but makes it much less
462
+    // likely('transport-info' is bundled with 'session-accept' and any
463
+    // immediate requests).
464
+    //
465
+    // this.connection.flush();
444 466
 };
445 467
 
446 468
 /**

+ 12
- 5
modules/xmpp/strophe.jingle.js Ver arquivo

@@ -6,7 +6,7 @@ var JingleSession = require("./JingleSessionPC");
6 6
 var XMPPEvents = require("../../service/xmpp/XMPPEvents");
7 7
 var RTCBrowserType = require("../RTC/RTCBrowserType");
8 8
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
9
-
9
+var Statistics = require("../statistics/statistics");
10 10
 
11 11
 module.exports = function(XMPP, eventEmitter) {
12 12
     Strophe.addConnectionPlugin('jingle', {
@@ -101,6 +101,8 @@ module.exports = function(XMPP, eventEmitter) {
101 101
                             .up();
102 102
                         this.terminate(sess.sid);
103 103
                     }
104
+                    Statistics.analytics.sendEvent(
105
+                        'xmpp.session-initiate', now);
104 106
                     break;
105 107
                 case 'session-terminate':
106 108
                     logger.log('terminating...', sess.sid);
@@ -114,13 +116,18 @@ module.exports = function(XMPP, eventEmitter) {
114 116
                     this.terminate(sess.sid, reasonCondition, reasonText);
115 117
                     break;
116 118
                 case 'transport-replace':
117
-                    logger.info("(TIME) Start transport replace",
118
-                                window.performance.now());
119
+                    var now = window.performance.now();
120
+                    logger.info("(TIME) Start transport replace", now);
121
+                    Statistics.analytics.sendEvent(
122
+                        'xmpp.transport-replace.start', now);
123
+
119 124
                     sess.replaceTransport($(iq).find('>jingle'),
120 125
                         function () {
126
+                            var now = window.performance.now();
121 127
                             logger.info(
122
-                                "(TIME) Transport replace success!",
123
-                                window.performance.now());
128
+                                "(TIME) Transport replace success!", now);
129
+                            Statistics.analytics.sendEvent(
130
+                                'xmpp.transport-replace.success', now);
124 131
                         },
125 132
                         function(error) {
126 133
                             GlobalOnErrorHandler.callErrorHandler(error);

+ 2
- 1
package.json Ver arquivo

@@ -27,7 +27,8 @@
27 27
     "jitsi-meet-logger": "git+https://github.com/jitsi/jitsi-meet-logger.git",
28 28
     "strophe": "^1.2.2",
29 29
     "strophejs-plugins": "^0.0.6",
30
-    "socket.io-client": "1.3.6"
30
+    "socket.io-client": "1.3.6",
31
+    "current-executing-script": "*"
31 32
   },
32 33
   "devDependencies": {
33 34
     "browserify": "11.1.x",

+ 22
- 0
service/RTC/CameraFacingMode.js Ver arquivo

@@ -0,0 +1,22 @@
1
+/* global module */
2
+/**
3
+ * The possible camera facing modes. For now support only 'user' and
4
+ * 'environment' because 'left' and 'right' are not used anywhere in our
5
+ * projects at the time of this writing. For more information please refer to
6
+ * https://w3c.github.io/mediacapture-main/getusermedia.html#def-constraint-facingMode
7
+ *
8
+ * @enum {string}
9
+ */
10
+var CameraFacingMode = {
11
+    /**
12
+     * The mode which specifies the environment-facing camera.
13
+     */
14
+    ENVIRONMENT: "environment",
15
+
16
+    /**
17
+     * The mode which specifies the user-facing camera.
18
+     */
19
+    USER: "user"
20
+};
21
+
22
+module.exports = CameraFacingMode;

+ 11
- 0
service/xmpp/XMPPEvents.js Ver arquivo

@@ -144,6 +144,17 @@ var XMPPEvents = {
144 144
      * Indicates that the local sendrecv streams in local SDP are changed.
145 145
      */
146 146
     SENDRECV_STREAMS_CHANGED: "xmpp.sendrecv_streams_changed",
147
+    /**
148
+     * Event fired when we do not get our 'session-accept' acknowledged by
149
+     * Jicofo. It most likely means that there is serious problem with our
150
+     * connection or XMPP server and we should reload the conference.
151
+     *
152
+     * We have seen that to happen in BOSH requests race condition when the BOSH
153
+     * request table containing the 'session-accept' was discarded by Prosody.
154
+     * Jicofo does send the RESULT immediately without any condition, so missing
155
+     * packets means that most likely it has never seen our IQ.
156
+     */
157
+    SESSION_ACCEPT_TIMEOUT: "xmpp.session_accept_timeout",
147 158
     // TODO: only used in a hack, should probably be removed.
148 159
     SET_LOCAL_DESCRIPTION_ERROR: 'xmpp.set_local_description_error',
149 160
 

Carregando…
Cancelar
Salvar