Browse Source

feat: report average RTP stats

AvgRTPStatsReporter will calculate arithmetic means of 'n' samples
and submit the values to the analytics module. The 'n' value is
configurable through 'avgRtpStatsN' conference config option. When set
to non-positive value the AvgRTPStatsReporter will be disabled.

The following values are reported:
- average upload bitrate => 'stat.avg.bitrate.upload'
- average download bitrate => 'stat.avg.bitrate.download'
- average upload bandwidth => 'stat.avg.bandwidth.upload'
- average download bandwidth => 'stat.avg.bandwidth.download'
- average total packet loss => 'stat.avg.packetloss.total'
- average upload packet loss => 'stat.avg.packetloss.upload'
- average download packet loss => 'stat.avg.packetloss.download'
- average FPS for remote videos => 'stat.avg.framerate.remote'
- average FPS for local video => 'stat.avg.framerate.local'
- average connection quality as defined by
  the ConnectionQuality module => 'stat.avg.cq'

If the conference runs in P2P mode 'p2p.' prefix will be added to
the event's name. Any pending calculations are wiped out on every switch
between P2P and JVB modes and samples have to be collected from
the start.
dev1
paweldomas 8 years ago
parent
commit
aee5d5c964
2 changed files with 386 additions and 0 deletions
  1. 15
    0
      JitsiConference.js
  2. 371
    0
      modules/statistics/AvgRTPStatsReporter.js

+ 15
- 0
JitsiConference.js View File

@@ -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
 

+ 371
- 0
modules/statistics/AvgRTPStatsReporter.js View File

@@ -0,0 +1,371 @@
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 Statistics from './statistics';
8
+
9
+const logger = getLogger(__filename);
10
+
11
+/**
12
+ * This will calculate an average for one, named stat and submit it to
13
+ * the analytics module when requested. It automatically counts the samples.
14
+ */
15
+class AverageStatReport {
16
+    /**
17
+     * Creates new <tt>AverageStatReport</tt> for given name.
18
+     * @param {string} name that's the name of the event that will be reported
19
+     * to the analytics module.
20
+     */
21
+    constructor(name) {
22
+        this.name = name;
23
+        this.count = 0;
24
+        this.sum = 0;
25
+    }
26
+
27
+    /**
28
+     * Adds the next value that will be included in the average when
29
+     * {@link calculate} is called.
30
+     * @param {number} nextValue
31
+     */
32
+    addNext(nextValue) {
33
+        if (typeof nextValue !== 'number') {
34
+            logger.error(
35
+                `${this.name} - invalid value for idx: ${this.count}`);
36
+
37
+            return;
38
+        }
39
+        this.sum += nextValue;
40
+        this.count += 1;
41
+    }
42
+
43
+    /**
44
+     * Calculates an average for the samples collected using {@link addNext}.
45
+     * @return {number|NaN} an average of all collected samples or <tt>NaN</tt>
46
+     * if no samples were collected.
47
+     */
48
+    calculate() {
49
+        return this.sum / this.count;
50
+    }
51
+
52
+    /**
53
+     * Calculates an average and submit the report to the analytics module.
54
+     * @param {boolean} isP2P indicates if the report is to be submitted for
55
+     * the P2P connection (when conference is currently in the P2P mode). This
56
+     * will add 'p2p.' prefix to the name of the event. All averages should be
57
+     * cleared when the conference switches, between P2P and JVB modes.
58
+     */
59
+    report(isP2P) {
60
+        Statistics.analytics.sendEvent(
61
+            `${isP2P ? 'p2p.' : ''}${this.name}`,
62
+            { value: this.calculate() });
63
+    }
64
+
65
+    /**
66
+     * Clears all memory of any samples collected, so that new average can be
67
+     * calculated using this instance.
68
+     */
69
+    reset() {
70
+        this.sum = 0;
71
+        this.count = 0;
72
+    }
73
+}
74
+
75
+/**
76
+ * Reports average RTP statistics values (arithmetic mean) to the analytics
77
+ * module for things like bit rate, bandwidth, packet loss etc. It keeps track
78
+ * of the P2P vs JVB conference modes and submits the values under different
79
+ * namespaces (the events for P2P mode have 'p2p.' prefix). Every switch between
80
+ * P2P mode resets the data collected so far and averages are calculated from
81
+ * scratch.
82
+ */
83
+export default class AvgRTPStatsReporter {
84
+    /**
85
+     * Creates new instance of <tt>AvgRTPStatsReporter</tt>
86
+     * @param {JitsiConference} conference
87
+     * @param {number} n the number of samples, before arithmetic mean is to be
88
+     * calculated and values submitted to the analytics module.
89
+     */
90
+    constructor(conference, n) {
91
+        /**
92
+         * How many {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED} samples
93
+         * are to be included in arithmetic mean calculation.
94
+         * @type {number}
95
+         * @private
96
+         */
97
+        this._n = n;
98
+
99
+        if (n > 0) {
100
+            logger.info(`Avg RTP stats will be calculated every ${n} samples`);
101
+        } else {
102
+            logger.info('Avg RTP stats reports are disabled.');
103
+
104
+            // Do not initialize
105
+            return;
106
+        }
107
+
108
+        /**
109
+         * The current sample index. Starts from 0 and goes up to {@link _n})
110
+         * when analytics report will be submitted.
111
+         * @type {number}
112
+         * @private
113
+         */
114
+        this._sampleIdx = 0;
115
+
116
+        /**
117
+         * The conference for which stats will be collected and reported.
118
+         * @type {JitsiConference}
119
+         * @private
120
+         */
121
+        this._conference = conference;
122
+
123
+        /**
124
+         * Average upload bitrate
125
+         * @type {AverageStatReport}
126
+         * @private
127
+         */
128
+        this._avgBitrateUp = new AverageStatReport('stat.avg.bitrate.upload');
129
+
130
+        /**
131
+         * Average download bitrate
132
+         * @type {AverageStatReport}
133
+         * @private
134
+         */
135
+        this._avgBitrateDown
136
+            = new AverageStatReport('stat.avg.bitrate.download');
137
+
138
+        /**
139
+         * Average upload bandwidth
140
+         * @type {AverageStatReport}
141
+         * @private
142
+         */
143
+        this._avgBandwidthUp
144
+            = new AverageStatReport('stat.avg.bandwidth.upload');
145
+
146
+        /**
147
+         * Average download bandwidth
148
+         * @type {AverageStatReport}
149
+         * @private
150
+         */
151
+        this._avgBandwidthDown
152
+            = new AverageStatReport('stat.avg.bandwidth.download');
153
+
154
+        /**
155
+         * Average total packet loss
156
+         * @type {AverageStatReport}
157
+         * @private
158
+         */
159
+        this._avgPacketLossTotal
160
+            = new AverageStatReport('stat.avg.packetloss.total');
161
+
162
+        /**
163
+         * Average upload packet loss
164
+         * @type {AverageStatReport}
165
+         * @private
166
+         */
167
+        this._avgPacketLossUp
168
+            = new AverageStatReport('stat.avg.packetloss.upload');
169
+
170
+        /**
171
+         * Average download packet loss
172
+         * @type {AverageStatReport}
173
+         * @private
174
+         */
175
+        this._avgPacketLossDown
176
+            = new AverageStatReport('stat.avg.packetloss.download');
177
+
178
+        /**
179
+         * Average FPS for remote videos
180
+         * @type {AverageStatReport}
181
+         * @private
182
+         */
183
+        this._avgRemoteFPS = new AverageStatReport('stat.avg.framerate.remote');
184
+
185
+        /**
186
+         * Average FPS for local video
187
+         * @type {AverageStatReport}
188
+         * @private
189
+         */
190
+        this._avgLocalFPS = new AverageStatReport('stat.avg.framerate.local');
191
+
192
+        /**
193
+         * Average connection quality as defined by
194
+         * the {@link ConnectionQuality} module.
195
+         * @type {AverageStatReport}
196
+         * @private
197
+         */
198
+        this._avgCQ = new AverageStatReport('stat.avg.cq');
199
+
200
+        this._onLocalStatsUpdated = data => this._calculateAvgStats(data);
201
+        conference.on(
202
+            ConnectionQualityEvents.LOCAL_STATS_UPDATED,
203
+            this._onLocalStatsUpdated);
204
+
205
+        this._onP2PStatusChanged = () => {
206
+            logger.debug('Resetting average stats calculation');
207
+            this._resetAvgStats();
208
+        };
209
+        conference.on(
210
+            ConferenceEvents.P2P_STATUS,
211
+            this._onP2PStatusChanged);
212
+    }
213
+
214
+    /**
215
+     * Processes next batch of stats reported on
216
+     * {@link ConnectionQualityEvents.LOCAL_STATS_UPDATED}.
217
+     * @param {go figure} data
218
+     * @private
219
+     */
220
+    _calculateAvgStats(data) {
221
+
222
+        const isP2P = this._conference.isP2PActive();
223
+        const peerCount = this._conference.getParticipants().length;
224
+
225
+        if (!isP2P && peerCount < 2) {
226
+
227
+            // There's no point in collecting stats for a JVB conference of 1.
228
+            // That happens for short period of time after everyone leaves
229
+            // the room, until Jicofo terminates the session.
230
+            return;
231
+        }
232
+
233
+        /* Uncomment to figure out stats structure
234
+        for (const key in data) {
235
+            if (data.hasOwnProperty(key)) {
236
+                logger.info(`local stat ${key}: `, data[key]);
237
+            }
238
+        } */
239
+
240
+        if (!data) {
241
+            logger.error('No stats');
242
+
243
+            return;
244
+        }
245
+
246
+        const bitrate = data.bitrate;
247
+        const bandwidth = data.bandwidth;
248
+        const packetLoss = data.packetLoss;
249
+        const frameRate = data.framerate;
250
+
251
+        if (!bitrate) {
252
+            logger.error('No "bitrate"');
253
+
254
+            return;
255
+        } else if (!bandwidth) {
256
+            logger.error('No "bandwidth"');
257
+
258
+            return;
259
+        } else if (!packetLoss) {
260
+            logger.error('No "packetloss"');
261
+
262
+            return;
263
+        } else if (!frameRate) {
264
+            logger.error('No "framerate"');
265
+
266
+            return;
267
+        }
268
+
269
+        this._avgBitrateUp.addNext(bitrate.upload);
270
+        this._avgBitrateDown.addNext(bitrate.download);
271
+        this._avgBandwidthUp.addNext(bandwidth.upload);
272
+        this._avgBandwidthDown.addNext(bandwidth.download);
273
+        this._avgPacketLossUp.addNext(packetLoss.upload);
274
+        this._avgPacketLossDown.addNext(packetLoss.download);
275
+        this._avgPacketLossTotal.addNext(packetLoss.total);
276
+        this._avgCQ.addNext(data.connectionQuality);
277
+
278
+        if (frameRate) {
279
+            this._avgRemoteFPS.addNext(
280
+                this._calculateAvgVideoFps(frameRate, false /* remote */));
281
+            this._avgLocalFPS.addNext(
282
+                this._calculateAvgVideoFps(frameRate, true /* local */));
283
+        }
284
+
285
+        this._sampleIdx += 1;
286
+
287
+        if (this._sampleIdx >= this._n) {
288
+            this._avgBitrateUp.report(isP2P);
289
+            this._avgBitrateDown.report(isP2P);
290
+            this._avgBandwidthUp.report(isP2P);
291
+            this._avgBandwidthDown.report(isP2P);
292
+            this._avgPacketLossUp.report(isP2P);
293
+            this._avgPacketLossDown.report(isP2P);
294
+            this._avgPacketLossTotal.report(isP2P);
295
+            this._avgRemoteFPS.report(isP2P);
296
+            this._avgLocalFPS.report(isP2P);
297
+            this._avgCQ.report(isP2P);
298
+
299
+            this._resetAvgStats();
300
+        }
301
+    }
302
+
303
+    /**
304
+     * Calculates average FPS for the report
305
+     * @param {go figure} frameRate
306
+     * @param {boolean} isLocal if the average is to be calculated for the local
307
+     * video or <tt>false</tt> if for remote videos.
308
+     * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
309
+     * @private
310
+     */
311
+    _calculateAvgVideoFps(frameRate, isLocal) {
312
+        let peerCount = 0;
313
+        let subFrameAvg = 0;
314
+        const myID = this._conference.myUserId();
315
+
316
+        for (const peerID of Object.keys(frameRate)) {
317
+            if (isLocal ? peerID === myID : peerID !== myID) {
318
+                const videos = frameRate[peerID];
319
+                const ssrcs = Object.keys(videos);
320
+
321
+                if (ssrcs.length) {
322
+                    let peerAvg = 0;
323
+
324
+                    for (const ssrc of ssrcs) {
325
+                        peerAvg += parseInt(videos[ssrc], 10);
326
+                    }
327
+
328
+                    peerAvg /= ssrcs.length;
329
+
330
+                    subFrameAvg += peerAvg;
331
+                    peerCount += 1;
332
+                }
333
+            }
334
+        }
335
+
336
+        return subFrameAvg / peerCount;
337
+    }
338
+
339
+    /**
340
+     * Reset cache of all averages and {@link _sampleIdx}.
341
+     * @private
342
+     */
343
+    _resetAvgStats() {
344
+        this._avgBitrateUp.reset();
345
+        this._avgBitrateDown.reset();
346
+        this._avgBandwidthUp.reset();
347
+        this._avgBandwidthDown.reset();
348
+        this._avgPacketLossUp.reset();
349
+        this._avgPacketLossDown.reset();
350
+        this._avgRemoteFPS.reset();
351
+        this._avgLocalFPS.reset();
352
+        this._avgCQ.reset();
353
+        this._sampleIdx = 0;
354
+    }
355
+
356
+    /**
357
+     * Unregisters all event listeners and stops working.
358
+     */
359
+    dispose() {
360
+        if (this._onP2PStatusChanged) {
361
+            this._conference.off(
362
+                ConferenceEvents.P2P_STATUS,
363
+                this._onP2PStatusChanged);
364
+        }
365
+        if (this._onLocalStatsUpdated) {
366
+            this._conference.off(
367
+                ConnectionQualityEvents.LOCAL_STATS_UPDATED,
368
+                this._onLocalStatsUpdated);
369
+        }
370
+    }
371
+}

Loading…
Cancel
Save