Explorar el Código

Merge pull request #482 from jitsi/analytics_avg_rtp_stats

Report average RTP stats to anayltics
dev1
George Politis hace 8 años
padre
commit
6f382660ba

+ 15
- 0
JitsiConference.js Ver fichero

@@ -1,5 +1,6 @@
1 1
 /* global __filename, Strophe, Promise */
2 2
 
3
+import AvgRTPStatsReporter from './modules/statistics/AvgRTPStatsReporter';
3 4
 import ComponentsVersions from './modules/version/ComponentsVersions';
4 5
 import ConnectionQuality from './modules/connectivity/ConnectionQuality';
5 6
 import { getLogger } from 'jitsi-meet-logger';
@@ -40,6 +41,9 @@ const logger = getLogger(__filename);
40 41
  * @param options.name the name of the conference
41 42
  * @param options.connection the JitsiConnection object for this
42 43
  * JitsiConference.
44
+ * @param {number} [options.config.avgRtpStatsN=15] how many samples are to be
45
+ * collected by {@link AvgRTPStatsReporter}, before arithmetic mean is
46
+ * calculated and submitted to the analytics module.
43 47
  * @param {boolean} [options.config.enableP2P] when set to <tt>true</tt>
44 48
  * the peer to peer mode will be enabled. It means that when there are only 2
45 49
  * participants in the conference an attempt to make direct connection will be
@@ -108,6 +112,13 @@ export default function JitsiConference(options) {
108 112
     this.connectionQuality
109 113
         = new ConnectionQuality(this, this.eventEmitter, options);
110 114
 
115
+    /**
116
+     * Reports average RTP statistics to the analytics module.
117
+     * @type {AvgRTPStatsReporter}
118
+     */
119
+    this.avgRtpStatsReporter
120
+        = new AvgRTPStatsReporter(this, options.config.avgRtpStatsN || 15);
121
+
111 122
     /**
112 123
      * Indicates whether the connection is interrupted or not.
113 124
      */
@@ -265,6 +276,10 @@ JitsiConference.prototype.leave = function() {
265 276
         this.participantConnectionStatus.dispose();
266 277
         this.participantConnectionStatus = null;
267 278
     }
279
+    if (this.avgRtpStatsReporter) {
280
+        this.avgRtpStatsReporter.dispose();
281
+        this.avgRtpStatsReporter = null;
282
+    }
268 283
 
269 284
     this.getLocalTracks().forEach(track => this.onLocalTrackRemoved(track));
270 285
 

+ 28
- 0
modules/RTC/RTCBrowserType.js Ver fichero

@@ -207,6 +207,34 @@ const RTCBrowserType = {
207 207
         return RTCBrowserType.isFirefox();
208 208
     },
209 209
 
210
+    /**
211
+     * Checks if the current browser reports upload and download bandwidth
212
+     * statistics.
213
+     * @return {boolean}
214
+     */
215
+    supportsBandwidthStatistics() {
216
+        // FIXME bandwidth stats are currently not implemented for FF on our
217
+        // side, but not sure if not possible ?
218
+        return !RTCBrowserType.isFirefox();
219
+    },
220
+
221
+    /**
222
+     * Checks if the current browser reports round trip time statistics for
223
+     * the ICE candidate pair.
224
+     * @return {boolean}
225
+     */
226
+    supportsRTTStatistics() {
227
+        // Firefox does not seem to report RTT for ICE candidate pair:
228
+        // eslint-disable-next-line max-len
229
+        // https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-currentroundtriptime
230
+        // It does report mozRTT for RTP streams, but at the time of this
231
+        // writing it's value does not make sense most of the time
232
+        // (is reported as 1):
233
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=1241066
234
+        // For Chrome and others we rely on 'googRtt'.
235
+        return !RTCBrowserType.isFirefox();
236
+    },
237
+
210 238
     /**
211 239
      * Whether jitsi-meet supports simulcast on the current browser.
212 240
      * @returns {boolean}

+ 7
- 2
modules/connectivity/ConnectionQuality.js Ver fichero

@@ -393,7 +393,11 @@ export default class ConnectionQuality {
393 393
         const data = {
394 394
             bitrate: this._localStats.bitrate,
395 395
             packetLoss: this._localStats.packetLoss,
396
-            connectionQuality: this._localStats.connectionQuality
396
+            connectionQuality: this._localStats.connectionQuality,
397
+            jvbRTT: this._localStats.transport
398
+                    && this._localStats.transport.length
399
+                    && !this._localStats.transport[0].p2p
400
+                        ? this._localStats.transport[0].rtt : undefined
397 401
         };
398 402
 
399 403
         // TODO: It looks like the remote participants don't really "care"
@@ -474,7 +478,8 @@ export default class ConnectionQuality {
474 478
         this._remoteStats[id] = {
475 479
             bitrate: data.bitrate,
476 480
             packetLoss: data.packetLoss,
477
-            connectionQuality: data.connectionQuality
481
+            connectionQuality: data.connectionQuality,
482
+            jvbRTT: data.jvbRTT
478 483
         };
479 484
 
480 485
         this.eventEmitter.emit(

+ 477
- 0
modules/statistics/AvgRTPStatsReporter.js Ver fichero

@@ -0,0 +1,477 @@
1
+/* global __filename */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+import * as ConnectionQualityEvents
5
+    from '../../service/connectivity/ConnectionQualityEvents';
6
+import * as ConferenceEvents from '../../JitsiConferenceEvents';
7
+import RTCBrowserType from '../RTC/RTCBrowserType';
8
+import Statistics from './statistics';
9
+
10
+const logger = getLogger(__filename);
11
+
12
+/**
13
+ * This will calculate an average for one, named stat and submit it to
14
+ * the analytics module when requested. It automatically counts the samples.
15
+ */
16
+class AverageStatReport {
17
+    /**
18
+     * Creates new <tt>AverageStatReport</tt> for given name.
19
+     * @param {string} name that's the name of the event that will be reported
20
+     * to the analytics module.
21
+     */
22
+    constructor(name) {
23
+        this.name = name;
24
+        this.count = 0;
25
+        this.sum = 0;
26
+    }
27
+
28
+    /**
29
+     * Adds the next value that will be included in the average when
30
+     * {@link calculate} is called.
31
+     * @param {number} nextValue
32
+     */
33
+    addNext(nextValue) {
34
+        if (typeof nextValue !== 'number') {
35
+            logger.error(
36
+                `${this.name} - invalid value for idx: ${this.count}`,
37
+                nextValue);
38
+
39
+            return;
40
+        }
41
+        this.sum += nextValue;
42
+        this.count += 1;
43
+    }
44
+
45
+    /**
46
+     * Calculates an average for the samples collected using {@link addNext}.
47
+     * @return {number|NaN} an average of all collected samples or <tt>NaN</tt>
48
+     * if no samples were collected.
49
+     */
50
+    calculate() {
51
+        return this.sum / this.count;
52
+    }
53
+
54
+    /**
55
+     * Calculates an average and submit the report to the analytics module.
56
+     * @param {boolean} isP2P indicates if the report is to be submitted for
57
+     * the P2P connection (when conference is currently in the P2P mode). This
58
+     * will add 'p2p.' prefix to the name of the event. All averages should be
59
+     * cleared when the conference switches, between P2P and JVB modes.
60
+     */
61
+    report(isP2P) {
62
+        Statistics.analytics.sendEvent(
63
+            `${isP2P ? 'p2p.' : ''}${this.name}`,
64
+            { value: this.calculate() });
65
+    }
66
+
67
+    /**
68
+     * Clears all memory of any samples collected, so that new average can be
69
+     * calculated using this instance.
70
+     */
71
+    reset() {
72
+        this.sum = 0;
73
+        this.count = 0;
74
+    }
75
+}
76
+
77
+/**
78
+ * Reports average RTP statistics values (arithmetic mean) to the analytics
79
+ * module for things like bit rate, bandwidth, packet loss etc. It keeps track
80
+ * of the P2P vs JVB conference modes and submits the values under different
81
+ * namespaces (the events for P2P mode have 'p2p.' prefix). Every switch between
82
+ * P2P mode resets the data collected so far and averages are calculated from
83
+ * scratch.
84
+ */
85
+export default class AvgRTPStatsReporter {
86
+    /**
87
+     * Creates new instance of <tt>AvgRTPStatsReporter</tt>
88
+     * @param {JitsiConference} conference
89
+     * @param {number} n the number of samples, before arithmetic mean is to be
90
+     * calculated and values submitted to the analytics module.
91
+     */
92
+    constructor(conference, n) {
93
+        /**
94
+         * How many {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED} samples
95
+         * are to be included in arithmetic mean calculation.
96
+         * @type {number}
97
+         * @private
98
+         */
99
+        this._n = n;
100
+
101
+        if (n > 0) {
102
+            logger.info(`Avg RTP stats will be calculated every ${n} samples`);
103
+        } else {
104
+            logger.info('Avg RTP stats reports are disabled.');
105
+
106
+            // Do not initialize
107
+            return;
108
+        }
109
+
110
+        /**
111
+         * The current sample index. Starts from 0 and goes up to {@link _n})
112
+         * when analytics report will be submitted.
113
+         * @type {number}
114
+         * @private
115
+         */
116
+        this._sampleIdx = 0;
117
+
118
+        /**
119
+         * The conference for which stats will be collected and reported.
120
+         * @type {JitsiConference}
121
+         * @private
122
+         */
123
+        this._conference = conference;
124
+
125
+        /**
126
+         * Average upload bitrate
127
+         * @type {AverageStatReport}
128
+         * @private
129
+         */
130
+        this._avgBitrateUp = new AverageStatReport('stat.avg.bitrate.upload');
131
+
132
+        /**
133
+         * Average download bitrate
134
+         * @type {AverageStatReport}
135
+         * @private
136
+         */
137
+        this._avgBitrateDown
138
+            = new AverageStatReport('stat.avg.bitrate.download');
139
+
140
+        /**
141
+         * Average upload bandwidth
142
+         * @type {AverageStatReport}
143
+         * @private
144
+         */
145
+        this._avgBandwidthUp
146
+            = new AverageStatReport('stat.avg.bandwidth.upload');
147
+
148
+        /**
149
+         * Average download bandwidth
150
+         * @type {AverageStatReport}
151
+         * @private
152
+         */
153
+        this._avgBandwidthDown
154
+            = new AverageStatReport('stat.avg.bandwidth.download');
155
+
156
+        /**
157
+         * Average total packet loss
158
+         * @type {AverageStatReport}
159
+         * @private
160
+         */
161
+        this._avgPacketLossTotal
162
+            = new AverageStatReport('stat.avg.packetloss.total');
163
+
164
+        /**
165
+         * Average upload packet loss
166
+         * @type {AverageStatReport}
167
+         * @private
168
+         */
169
+        this._avgPacketLossUp
170
+            = new AverageStatReport('stat.avg.packetloss.upload');
171
+
172
+        /**
173
+         * Average download packet loss
174
+         * @type {AverageStatReport}
175
+         * @private
176
+         */
177
+        this._avgPacketLossDown
178
+            = new AverageStatReport('stat.avg.packetloss.download');
179
+
180
+        /**
181
+         * Average FPS for remote videos
182
+         * @type {AverageStatReport}
183
+         * @private
184
+         */
185
+        this._avgRemoteFPS = new AverageStatReport('stat.avg.framerate.remote');
186
+
187
+        /**
188
+         * Map stores average RTT to the JVB reported by remote participants.
189
+         * Mapped per participant id {@link JitsiParticipant.getId}.
190
+         * @type {Map<string,AverageStatReport>}
191
+         * @private
192
+         */
193
+        this._avgRemoteRTTMap = new Map();
194
+
195
+        /**
196
+         * Average round trip time reported by the ICE candidate pair.
197
+         * FIXME currently reported only for P2P
198
+         * @type {AverageStatReport}
199
+         * @private
200
+         */
201
+        this._avgRTT = new AverageStatReport('stat.avg.rtt');
202
+
203
+        /**
204
+         * Average FPS for local video
205
+         * @type {AverageStatReport}
206
+         * @private
207
+         */
208
+        this._avgLocalFPS = new AverageStatReport('stat.avg.framerate.local');
209
+
210
+        /**
211
+         * Average connection quality as defined by
212
+         * the {@link ConnectionQuality} module.
213
+         * @type {AverageStatReport}
214
+         * @private
215
+         */
216
+        this._avgCQ = new AverageStatReport('stat.avg.cq');
217
+
218
+        this._onLocalStatsUpdated = data => this._calculateAvgStats(data);
219
+        conference.on(
220
+            ConnectionQualityEvents.LOCAL_STATS_UPDATED,
221
+            this._onLocalStatsUpdated);
222
+
223
+        this._onRemoteStatsUpdated
224
+            = (id, data) => this._processRemoteStats(id, data);
225
+        conference.on(
226
+            ConnectionQualityEvents.REMOTE_STATS_UPDATED,
227
+            this._onRemoteStatsUpdated);
228
+
229
+        this._onP2PStatusChanged = () => {
230
+            logger.debug('Resetting average stats calculation');
231
+            this._resetAvgStats();
232
+        };
233
+        conference.on(
234
+            ConferenceEvents.P2P_STATUS,
235
+            this._onP2PStatusChanged);
236
+
237
+        this._onUserLeft = id => this._avgRemoteRTTMap.delete(id);
238
+        conference.on(ConferenceEvents.USER_LEFT, this._onUserLeft);
239
+    }
240
+
241
+    /**
242
+     * Calculates arithmetic mean of all RTTs towards the JVB reported by
243
+     * participants.
244
+     * @return {number|NaN} NaN if not available (not enough data)
245
+     * @private
246
+     */
247
+    _calculateAvgRemoteRTT() {
248
+        let count = 0, sum = 0;
249
+
250
+        // FIXME should we ignore RTT for participant
251
+        // who "is having connectivity issues" ?
252
+        for (const remoteAvg of this._avgRemoteRTTMap.values()) {
253
+            const avg = remoteAvg.calculate();
254
+
255
+            if (!isNaN(avg)) {
256
+                sum += avg;
257
+                count += 1;
258
+                remoteAvg.reset();
259
+            }
260
+        }
261
+
262
+        return sum / count;
263
+    }
264
+
265
+    /**
266
+     * Processes next batch of stats reported on
267
+     * {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED}.
268
+     * @param {go figure} data
269
+     * @private
270
+     */
271
+    _calculateAvgStats(data) {
272
+
273
+        const isP2P = this._conference.isP2PActive();
274
+        const peerCount = this._conference.getParticipants().length;
275
+
276
+        if (!isP2P && peerCount < 1) {
277
+
278
+            // There's no point in collecting stats for a JVB conference of 1.
279
+            // That happens for short period of time after everyone leaves
280
+            // the room, until Jicofo terminates the session.
281
+            return;
282
+        }
283
+
284
+        /* Uncomment to figure out stats structure
285
+        for (const key in data) {
286
+            if (data.hasOwnProperty(key)) {
287
+                logger.info(`local stat ${key}: `, data[key]);
288
+            }
289
+        } */
290
+
291
+        if (!data) {
292
+            logger.error('No stats');
293
+
294
+            return;
295
+        }
296
+
297
+        const bitrate = data.bitrate;
298
+        const bandwidth = data.bandwidth;
299
+        const packetLoss = data.packetLoss;
300
+        const frameRate = data.framerate;
301
+
302
+        if (!bitrate) {
303
+            logger.error('No "bitrate"');
304
+
305
+            return;
306
+        } else if (!bandwidth) {
307
+            logger.error('No "bandwidth"');
308
+
309
+            return;
310
+        } else if (!packetLoss) {
311
+            logger.error('No "packetloss"');
312
+
313
+            return;
314
+        } else if (!frameRate) {
315
+            logger.error('No "framerate"');
316
+
317
+            return;
318
+        }
319
+
320
+        this._avgBitrateUp.addNext(bitrate.upload);
321
+        this._avgBitrateDown.addNext(bitrate.download);
322
+
323
+        if (RTCBrowserType.supportsBandwidthStatistics()) {
324
+            this._avgBandwidthUp.addNext(bandwidth.upload);
325
+            this._avgBandwidthDown.addNext(bandwidth.download);
326
+        }
327
+
328
+        this._avgPacketLossUp.addNext(packetLoss.upload);
329
+        this._avgPacketLossDown.addNext(packetLoss.download);
330
+        this._avgPacketLossTotal.addNext(packetLoss.total);
331
+        this._avgCQ.addNext(data.connectionQuality);
332
+
333
+        if (RTCBrowserType.supportsRTTStatistics()) {
334
+            if (data.transport && data.transport.length) {
335
+                this._avgRTT.addNext(data.transport[0].rtt);
336
+            } else {
337
+                this._avgRTT.reset();
338
+            }
339
+        }
340
+
341
+        if (frameRate) {
342
+            this._avgRemoteFPS.addNext(
343
+                this._calculateAvgVideoFps(frameRate, false /* remote */));
344
+            this._avgLocalFPS.addNext(
345
+                this._calculateAvgVideoFps(frameRate, true /* local */));
346
+        }
347
+
348
+        this._sampleIdx += 1;
349
+
350
+        if (this._sampleIdx >= this._n) {
351
+            this._avgBitrateUp.report(isP2P);
352
+            this._avgBitrateDown.report(isP2P);
353
+            if (RTCBrowserType.supportsBandwidthStatistics()) {
354
+                this._avgBandwidthUp.report(isP2P);
355
+                this._avgBandwidthDown.report(isP2P);
356
+            }
357
+            this._avgPacketLossUp.report(isP2P);
358
+            this._avgPacketLossDown.report(isP2P);
359
+            this._avgPacketLossTotal.report(isP2P);
360
+            this._avgRemoteFPS.report(isP2P);
361
+            this._avgLocalFPS.report(isP2P);
362
+            this._avgCQ.report(isP2P);
363
+
364
+            if (RTCBrowserType.supportsRTTStatistics()) {
365
+                this._avgRTT.report(isP2P);
366
+                if (!isP2P) {
367
+                    const avgRemoteRTT = this._calculateAvgRemoteRTT();
368
+                    const avgLocalRTT = this._avgRTT.calculate();
369
+
370
+                    if (!isNaN(avgLocalRTT) && !isNaN(avgRemoteRTT)) {
371
+                        Statistics.analytics.sendEvent(
372
+                            'stat.avg.end2endrtt',
373
+                            avgLocalRTT + avgRemoteRTT);
374
+                    }
375
+                }
376
+            }
377
+
378
+            this._resetAvgStats();
379
+        }
380
+    }
381
+
382
+    /**
383
+     * Calculates average FPS for the report
384
+     * @param {go figure} frameRate
385
+     * @param {boolean} isLocal if the average is to be calculated for the local
386
+     * video or <tt>false</tt> if for remote videos.
387
+     * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
388
+     * @private
389
+     */
390
+    _calculateAvgVideoFps(frameRate, isLocal) {
391
+        let peerCount = 0;
392
+        let subFrameAvg = 0;
393
+        const myID = this._conference.myUserId();
394
+
395
+        for (const peerID of Object.keys(frameRate)) {
396
+            if (isLocal ? peerID === myID : peerID !== myID) {
397
+                const videos = frameRate[peerID];
398
+                const ssrcs = Object.keys(videos);
399
+
400
+                if (ssrcs.length) {
401
+                    let peerAvg = 0;
402
+
403
+                    for (const ssrc of ssrcs) {
404
+                        peerAvg += parseInt(videos[ssrc], 10);
405
+                    }
406
+
407
+                    peerAvg /= ssrcs.length;
408
+
409
+                    subFrameAvg += peerAvg;
410
+                    peerCount += 1;
411
+                }
412
+            }
413
+        }
414
+
415
+        return subFrameAvg / peerCount;
416
+    }
417
+
418
+    /**
419
+     * Processes {@link ConnectionQualityEvents.REMOTE_STATS_UPDATED} to analyse
420
+     * RTT towards the JVB reported by each participant.
421
+     * @param {string} id {@link JitsiParticipant.getId}
422
+     * @param {go figure in ConnectionQuality.js} data
423
+     * @private
424
+     */
425
+    _processRemoteStats(id, data) {
426
+        const validData = typeof data.jvbRTT === 'number';
427
+        let rttAvg = this._avgRemoteRTTMap.get(id);
428
+
429
+        if (!rttAvg && validData) {
430
+            rttAvg = new AverageStatReport(`${id}.stat.rtt`);
431
+            this._avgRemoteRTTMap.set(id, rttAvg);
432
+        }
433
+
434
+        if (validData) {
435
+            rttAvg.addNext(data.jvbRTT);
436
+        } else if (rttAvg) {
437
+            this._avgRemoteRTTMap.delete(id);
438
+        }
439
+    }
440
+
441
+    /**
442
+     * Reset cache of all averages and {@link _sampleIdx}.
443
+     * @private
444
+     */
445
+    _resetAvgStats() {
446
+        this._avgBitrateUp.reset();
447
+        this._avgBitrateDown.reset();
448
+        this._avgBandwidthUp.reset();
449
+        this._avgBandwidthDown.reset();
450
+        this._avgPacketLossUp.reset();
451
+        this._avgPacketLossDown.reset();
452
+        this._avgRemoteFPS.reset();
453
+        this._avgLocalFPS.reset();
454
+        this._avgCQ.reset();
455
+        this._avgRTT.reset();
456
+        this._avgRemoteRTTMap.clear();
457
+        this._sampleIdx = 0;
458
+    }
459
+
460
+    /**
461
+     * Unregisters all event listeners and stops working.
462
+     */
463
+    dispose() {
464
+        this._conference.off(
465
+            ConferenceEvents.P2P_STATUS,
466
+            this._onP2PStatusChanged);
467
+        this._conference.off(
468
+            ConnectionQualityEvents.LOCAL_STATS_UPDATED,
469
+            this._onLocalStatsUpdated);
470
+        this._conference.off(
471
+            ConnectionQualityEvents.REMOTE_STATS_UPDATED,
472
+            this._onRemoteStatsUpdated);
473
+        this._conference.off(
474
+            ConferenceEvents.USER_LEFT,
475
+            this._onUserLeft);
476
+    }
477
+}

+ 6
- 3
modules/statistics/RTPStatsCollector.js Ver fichero

@@ -44,7 +44,8 @@ KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME] = {
44 44
     'googFrameRateReceived': 'googFrameRateReceived',
45 45
     'googFrameRateSent': 'googFrameRateSent',
46 46
     'audioInputLevel': 'audioInputLevel',
47
-    'audioOutputLevel': 'audioOutputLevel'
47
+    'audioOutputLevel': 'audioOutputLevel',
48
+    'currentRoundTripTime': 'googRtt'
48 49
 };
49 50
 KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_OPERA]
50 51
     = KEYS_BY_BROWSER_TYPE[RTCBrowserType.RTC_BROWSER_CHROME];
@@ -456,13 +457,14 @@ StatsCollector.prototype.processStatsReport = function() {
456 457
         } catch (e) { /* not supported*/ }
457 458
 
458 459
         if (now.type === 'googCandidatePair') {
459
-            let active, ip, localip, type;
460
+            let active, ip, localip, rtt, type;
460 461
 
461 462
             try {
462 463
                 ip = getStatValue(now, 'remoteAddress');
463 464
                 type = getStatValue(now, 'transportType');
464 465
                 localip = getStatValue(now, 'localAddress');
465 466
                 active = getStatValue(now, 'activeConnection');
467
+                rtt = getNonNegativeStat(now, 'currentRoundTripTime');
466 468
             } catch (e) { /* not supported*/ }
467 469
             if (!ip || !type || !localip || active !== 'true') {
468 470
                 continue;
@@ -480,7 +482,8 @@ StatsCollector.prototype.processStatsReport = function() {
480 482
                     ip,
481 483
                     type,
482 484
                     localip,
483
-                    p2p: this.peerconnection.isP2P
485
+                    p2p: this.peerconnection.isP2P,
486
+                    rtt
484 487
                 });
485 488
             }
486 489
             continue;

Loading…
Cancelar
Guardar