Browse Source

More p2p analytics (#487)

* feat: multiple, simultaneous RTP stats

Makes it possible to have remote RTP stats running for more than one
peerconnection at a time.

* feat(stats): report RTT all the time

Will report JVB RTT (and end to end) while in P2P mode and vice versa.

* fix(JitsiConferenceEvents): remove CONNECTION_STATS

CONNECTION_STATS event is no longer emitted.

* fix(AvgRTPStatsReported): users with no video

Do not include FPS == 0 in average remote FPS calculation. Report NaN
for local FPS when video muted or no video device. NaN will be reported
for avg remote FPS if no video is received.

* fix(AvgRTPStatsReported): reset total packet loss

* feat(AvgRTPStatsReported): report 'screen' FPS

Will report average FPS for screen videos separately from camera videos,
but only if available (camera video reports NaN FPS when not available).

* fix(AvgRTPStatsReported): end2endRTT

Needs to report JSON with value.

* feat(AVG RTP stats): separate audio and video bitrate

Will report average audio and video bitrates separately.

* doc(JitsiConference): try to improve comment

* fix(AvgRTPStatsReporter): remove confusing reset

There's no a clear reason for doing reset there.

* ref(AvgRTPStatsReporter): rename var
dev1
Paweł Domas 8 years ago
parent
commit
9cccf3b62e

+ 31
- 34
JitsiConference.js View File

@@ -595,13 +595,24 @@ JitsiConference.prototype.addTrack = function(track) {
595 595
 };
596 596
 
597 597
 /**
598
- * Fires TRACK_AUDIO_LEVEL_CHANGED change conference event.
598
+ * Fires TRACK_AUDIO_LEVEL_CHANGED change conference event (for local tracks).
599
+ * @param {TraceablePeerConnection|null} tpc
599 600
  * @param audioLevel the audio level
600 601
  */
601
-JitsiConference.prototype._fireAudioLevelChangeEvent = function(audioLevel) {
602
-    this.eventEmitter.emit(
603
-        JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
604
-        this.myUserId(), audioLevel);
602
+JitsiConference.prototype._fireAudioLevelChangeEvent
603
+= function(tpc, audioLevel) {
604
+    const activeTpc = this.getActivePeerConnection();
605
+
606
+    // There will be no TraceablePeerConnection if audio levels do not come from
607
+    // a peerconnection. LocalStatsCollector.js measures audio levels using Web
608
+    // Audio Analyser API and emits local audio levels events through
609
+    // JitsiTrack.setAudioLevel, but does not provide TPC instance which is
610
+    // optional.
611
+    if (tpc === null || activeTpc === tpc) {
612
+        this.eventEmitter.emit(
613
+            JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
614
+            this.myUserId(), audioLevel);
615
+    }
605 616
 };
606 617
 
607 618
 /**
@@ -1151,11 +1162,15 @@ JitsiConference.prototype.onRemoteTrackAdded = function(track) {
1151 1162
         () => emitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track));
1152 1163
     track.addEventListener(
1153 1164
         JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
1154
-        audioLevel => {
1155
-            emitter.emit(
1156
-                JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
1157
-                id,
1158
-                audioLevel);
1165
+        (tpc, audioLevel) => {
1166
+            const activeTPC = this.getActivePeerConnection();
1167
+
1168
+            if (activeTPC === tpc) {
1169
+                emitter.emit(
1170
+                    JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED,
1171
+                    id,
1172
+                    audioLevel);
1173
+            }
1159 1174
         }
1160 1175
     );
1161 1176
 
@@ -1330,7 +1345,7 @@ JitsiConference.prototype.onIncomingCall
1330 1345
         this.statistics.startCallStats(
1331 1346
             this.jvbJingleSession.peerconnection,
1332 1347
             'jitsi' /* Remote user ID for JVB is 'jitsi' */);
1333
-        this._startRemoteStats();
1348
+        this.statistics.startRemoteStats(this.jvbJingleSession.peerconnection);
1334 1349
     } catch (e) {
1335 1350
         GlobalOnErrorHandler.callErrorHandler(e);
1336 1351
         logger.error(e);
@@ -1385,19 +1400,6 @@ JitsiConference.prototype._rejectIncomingCall
1385 1400
         });
1386 1401
 };
1387 1402
 
1388
-/**
1389
- * Method called to start remote stats for the current peer connection (if
1390
- * available).
1391
- * @private
1392
- */
1393
-JitsiConference.prototype._startRemoteStats = function() {
1394
-    const activePeerConnection = this.getActivePeerConnection();
1395
-
1396
-    if (activePeerConnection) {
1397
-        this.statistics.startRemoteStats(activePeerConnection);
1398
-    }
1399
-};
1400
-
1401 1403
 /**
1402 1404
  * Handles the call ended event.
1403 1405
  * @param {JingleSessionPC} jingleSession the jingle session which has been
@@ -1419,7 +1421,8 @@ JitsiConference.prototype.onCallEnded
1419 1421
 
1420 1422
         // Stop the stats
1421 1423
         if (this.statistics) {
1422
-            this.statistics.stopRemoteStats();
1424
+            this.statistics.stopRemoteStats(
1425
+                this.jvbJingleSession.peerconnection);
1423 1426
             logger.info('Stopping JVB CallStats');
1424 1427
             this.statistics.stopCallStats(
1425 1428
                 this.jvbJingleSession.peerconnection);
@@ -1623,7 +1626,7 @@ JitsiConference.prototype.getPhonePin = function() {
1623 1626
  *
1624 1627
  * @return {TraceablePeerConnection|null} null if there isn't any active
1625 1628
  * <tt>TraceablePeerConnection</tt> currently available.
1626
- * @protected
1629
+ * @public (FIXME how to make package local ?)
1627 1630
  */
1628 1631
 JitsiConference.prototype.getActivePeerConnection = function() {
1629 1632
     if (this.isP2PActive()) {
@@ -2001,7 +2004,7 @@ JitsiConference.prototype._onIceConnectionEstablished
2001 2004
 
2002 2005
     // Start remote stats
2003 2006
     logger.info('Starting remote stats with p2p connection');
2004
-    this._startRemoteStats();
2007
+    this.statistics.startRemoteStats(this.p2pJingleSession.peerconnection);
2005 2008
 
2006 2009
     // Log the P2P established event
2007 2010
     if (this.p2pJingleSession.isInitiator) {
@@ -2276,7 +2279,7 @@ JitsiConference.prototype._stopP2PSession
2276 2279
 
2277 2280
     // Stop P2P stats
2278 2281
     logger.info('Stopping remote stats for P2P connection');
2279
-    this.statistics.stopRemoteStats();
2282
+    this.statistics.stopRemoteStats(this.p2pJingleSession.peerconnection);
2280 2283
     logger.info('Stopping CallStats for P2P connection');
2281 2284
     this.statistics.stopCallStats(
2282 2285
         this.p2pJingleSession.peerconnection);
@@ -2309,12 +2312,6 @@ JitsiConference.prototype._stopP2PSession
2309 2312
             logger.info('Not adding remote JVB tracks - no session yet');
2310 2313
         }
2311 2314
     }
2312
-
2313
-    // Start remote stats
2314
-    logger.info('Starting remote stats with JVB connection');
2315
-    if (this.jvbJingleSession) {
2316
-        this._startRemoteStats();
2317
-    }
2318 2315
 };
2319 2316
 
2320 2317
 /**

+ 5
- 64
JitsiConferenceEventManager.js View File

@@ -47,59 +47,6 @@ export default function JitsiConferenceEventManager(conference) {
47 47
         Statistics.sendEventToAll.bind(Statistics, 'connection.restored'));
48 48
 }
49 49
 
50
-/**
51
- * Groups resolutions by user id, skip incorrect resolutions.
52
- * @param conference {JitsiConference} the conference
53
- * @param resolutions map of resolutions by ssrc
54
- */
55
-function mapResolutionsByUserId(conference, resolutions) {
56
-    const id2resolution = {};
57
-
58
-    // preprocess resolutions: group by user id, skip incorrect
59
-    // resolutions etc.
60
-    for (const [ ssrc, resolution ] of resolutions) {
61
-        const id = conference.rtc.getResourceBySSRC(ssrc);
62
-
63
-        if (id
64
-            && resolution.width && resolution.height
65
-            && resolution.width !== -1 && resolution.height !== -1) {
66
-            // ssrc to resolution map for user id
67
-            const idResolutions = id2resolution[id] || {};
68
-
69
-            idResolutions[ssrc] = resolution;
70
-
71
-            id2resolution[id] = idResolutions;
72
-        }
73
-    }
74
-
75
-    return id2resolution;
76
-}
77
-
78
-/**
79
- * Groups framerates by user id, skip framerates with value of 0.
80
- * @param conference {JitsiConference} the conference
81
- * @param framerates map of framerates by ssrc
82
- */
83
-function mapFrameratesByUserId(conference, framerates) {
84
-    const id2framerate = {};
85
-
86
-    // preprocess framerates: group by user id
87
-    for (const [ ssrc, framerate ] of framerates) {
88
-        const id = conference.rtc.getResourceBySSRC(ssrc);
89
-
90
-        if (framerate !== 0 && id) {
91
-            // ssrc to framerate map for user id
92
-            const id2framerates = id2framerate[id] || {};
93
-
94
-            id2framerates[ssrc] = framerate;
95
-
96
-            id2framerate[id] = id2framerates;
97
-        }
98
-    }
99
-
100
-    return id2framerate;
101
-}
102
-
103 50
 /**
104 51
  * Setups event listeners related to conference.chatRoom
105 52
  */
@@ -621,24 +568,18 @@ JitsiConferenceEventManager.prototype.setupStatisticsListeners = function() {
621 568
         return;
622 569
     }
623 570
 
624
-    conference.statistics.addAudioLevelListener((ssrc, level) => {
625
-        conference.rtc.setAudioLevel(ssrc, level);
571
+    /* eslint-disable max-params */
572
+    conference.statistics.addAudioLevelListener((tpc, ssrc, level, isLocal) => {
573
+        conference.rtc.setAudioLevel(tpc, ssrc, level, isLocal);
626 574
     });
627 575
 
576
+    /* eslint-enable max-params */
577
+
628 578
     // Forward the "before stats disposed" event
629 579
     conference.statistics.addBeforeDisposedListener(() => {
630 580
         conference.eventEmitter.emit(
631 581
             JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED);
632 582
     });
633
-    conference.statistics.addConnectionStatsListener(stats => {
634
-
635
-        stats.resolution = mapResolutionsByUserId(conference, stats.resolution);
636
-        stats.framerate = mapFrameratesByUserId(conference, stats.framerate);
637
-
638
-        conference.eventEmitter.emit(
639
-            JitsiConferenceEvents.CONNECTION_STATS, stats);
640
-    });
641
-
642 583
     conference.statistics.addByteSentStatsListener((tpc, stats) => {
643 584
         conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
644 585
             const ssrc = tpc.getLocalSSRC(track);

+ 0
- 6
JitsiConferenceEvents.js View File

@@ -56,12 +56,6 @@ export const CONNECTION_INTERRUPTED = 'conference.connectionInterrupted';
56 56
  */
57 57
 export const CONNECTION_RESTORED = 'conference.connectionRestored';
58 58
 
59
-/**
60
- * New local connection statistics are received.
61
- * @deprecated Use ConnectionQualityEvents.LOCAL_STATS_UPDATED instead.
62
- */
63
-export const CONNECTION_STATS = 'conference.connectionStats';
64
-
65 59
 /**
66 60
  * A user has changed it display name
67 61
  */

+ 2
- 1
JitsiMeetJS.js View File

@@ -273,7 +273,8 @@ export default {
273 273
 
274 274
                         if (track.getType() === MediaType.AUDIO) {
275 275
                             Statistics.startLocalStats(mStream,
276
-                                track.setAudioLevel.bind(track));
276
+                                track.setAudioLevel.bind(
277
+                                    track, null /* no TPC */));
277 278
                             track.addEventListener(
278 279
                                 JitsiTrackEvents.LOCAL_TRACK_STOPPED,
279 280
                                 () => {

+ 9
- 0
JitsiParticipant.js View File

@@ -121,6 +121,15 @@ export default class JitsiParticipant {
121 121
         return this._tracks.slice();
122 122
     }
123 123
 
124
+    /**
125
+     * @param {MediaType} mediaType
126
+     * @returns {Array.<JitsiTrack>} an array of media tracks for this
127
+     * participant, for given media type.
128
+     */
129
+    getTracksByMediaType(mediaType) {
130
+        return this.getTracks().filter(track => track.getType() === mediaType);
131
+    }
132
+
124 133
     /**
125 134
      * @returns {String} The ID of this participant.
126 135
      */

+ 10
- 4
modules/RTC/JitsiTrack.js View File

@@ -381,13 +381,19 @@ JitsiTrack.prototype.removeEventListener = JitsiTrack.prototype.off;
381 381
 
382 382
 /**
383 383
  * Sets the audio level for the stream
384
- * @param audioLevel the new audio level
384
+ * @param {TraceablePeerConnection|null} tpc the peerconnection instance which
385
+ * is source for the audio level. It can be <tt>null</tt> for a local track if
386
+ * the audio level was measured outside of the peerconnection
387
+ * (see /modules/statistics/LocalStatsCollector.js).
388
+ * @param {number} audioLevel value between 0 and 1
385 389
  */
386
-JitsiTrack.prototype.setAudioLevel = function(audioLevel) {
390
+JitsiTrack.prototype.setAudioLevel = function(tpc, audioLevel) {
387 391
     if (this.audioLevel !== audioLevel) {
388
-        this.eventEmitter.emit(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
389
-            audioLevel);
390 392
         this.audioLevel = audioLevel;
393
+        this.eventEmitter.emit(
394
+            JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
395
+            tpc,
396
+            audioLevel);
391 397
     }
392 398
 };
393 399
 

+ 13
- 51
modules/RTC/RTC.js View File

@@ -647,70 +647,32 @@ export default class RTC extends Listenable {
647 647
         }
648 648
     }
649 649
 
650
+    /* eslint-disable max-params */
650 651
     /**
651 652
      *
652
-     * @param resource
653
-     * @param audioLevel
653
+     * @param {TraceablePeerConnection} tpc
654
+     * @param {number} ssrc
655
+     * @param {number} audioLevel
656
+     * @param {boolean} isLocal
654 657
      */
655
-    setAudioLevel(ssrc, audioLevel) {
656
-        const track = this._getTrackBySSRC(ssrc);
658
+    setAudioLevel(tpc, ssrc, audioLevel, isLocal) {
659
+        const track = tpc.getTrackBySSRC(ssrc);
657 660
 
658 661
         if (!track) {
659 662
             return;
660
-        }
661
-        if (!track.isAudioTrack()) {
663
+        } else if (!track.isAudioTrack()) {
662 664
             logger.warn(`Received audio level for non-audio track: ${ssrc}`);
663 665
 
664 666
             return;
667
+        } else if (track.isLocal() !== isLocal) {
668
+            logger.error(
669
+                `${track} was expected to ${isLocal ? 'be' : 'not be'} local`);
665 670
         }
666 671
 
667
-        track.setAudioLevel(audioLevel);
668
-    }
669
-
670
-    /**
671
-     * Searches in localTracks(session stores ssrc for audio and video) and
672
-     * remoteTracks for the ssrc and returns the corresponding resource.
673
-     * @param {number} ssrc the ssrc to check.
674
-     */
675
-    getResourceBySSRC(ssrc) {
676
-        const track = this._getTrackBySSRC(ssrc);
677
-
678
-        return track ? track.getParticipantId() : null;
672
+        track.setAudioLevel(tpc, audioLevel);
679 673
     }
680 674
 
681
-    /**
682
-     * Finds a track (either local or remote) which runs on the given SSRC.
683
-     * @param {number} ssrc
684
-     * @return {JitsiTrack|undefined}
685
-     * @private
686
-     */
687
-    _getTrackBySSRC(ssrc) {
688
-        let track
689
-            = this.getLocalTracks().find(
690
-                localTrack =>
691
-                    Array.from(this.peerConnections.values())
692
-                         .find(pc => pc.getLocalSSRC(localTrack) === ssrc)
693
-                );
694
-
695
-        if (!track) {
696
-            track = this._getRemoteTrackBySSRC(ssrc);
697
-        }
698
-
699
-        return track;
700
-    }
701
-
702
-    /**
703
-     * Searches in remoteTracks for the ssrc and returns the corresponding
704
-     * track.
705
-     * @param {number} ssrc the ssrc to check.
706
-     * @return {JitsiRemoteTrack|undefined} return the first remote track that
707
-     * matches given SSRC or <tt>undefined</tt> if no such track was found.
708
-     * @private
709
-     */
710
-    _getRemoteTrackBySSRC(ssrc) {
711
-        return this.getRemoteTracks().find(
712
-            remoteTrack => ssrc === remoteTrack.getSSRC());
713
-    }
675
+    /* eslint-enable max-params */
714 676
 
715 677
     /**
716 678
      * Sends message via the datachannels.

+ 24
- 0
modules/RTC/TraceablePeerConnection.js View File

@@ -473,6 +473,30 @@ TraceablePeerConnection.prototype.getRemoteTracks
473 473
     return remoteTracks;
474 474
 };
475 475
 
476
+/**
477
+ * Tries to find {@link JitsiTrack} for given SSRC number. It will search both
478
+ * local and remote tracks bound to this instance.
479
+ * @param {number} ssrc
480
+ * @return {JitsiTrack|null}
481
+ */
482
+TraceablePeerConnection.prototype.getTrackBySSRC = function(ssrc) {
483
+    if (typeof ssrc !== 'number') {
484
+        throw new Error(`SSRC ${ssrc} is not a number`);
485
+    }
486
+    for (const localTrack of this.localTracks.values()) {
487
+        if (this.getLocalSSRC(localTrack) === ssrc) {
488
+            return localTrack;
489
+        }
490
+    }
491
+    for (const remoteTrack of this.getRemoteTracks()) {
492
+        if (remoteTrack.getSSRC() === ssrc) {
493
+            return remoteTrack;
494
+        }
495
+    }
496
+
497
+    return null;
498
+};
499
+
476 500
 /**
477 501
  * Called when new remote MediaStream is added to the PeerConnection.
478 502
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant

+ 24
- 8
modules/connectivity/ConnectionQuality.js View File

@@ -161,7 +161,10 @@ export default class ConnectionQuality {
161 161
         /**
162 162
          * Holds statistics about the local connection quality.
163 163
          */
164
-        this._localStats = { connectionQuality: 100 };
164
+        this._localStats = {
165
+            connectionQuality: 100,
166
+            jvbRTT: undefined
167
+        };
165 168
 
166 169
         /**
167 170
          * The time this._localStats.connectionQuality was last updated.
@@ -232,8 +235,7 @@ export default class ConnectionQuality {
232 235
         // jitsi-meet
233 236
         // TODO: We should keep track of the remote resolution in _remoteStats,
234 237
         // and notify about changes via separate events.
235
-        conference.on(
236
-            ConferenceEvents.CONNECTION_STATS,
238
+        conference.statistics.addConnectionStatsListener(
237 239
             this._updateLocalStats.bind(this));
238 240
 
239 241
         // Save the last time we were unmuted.
@@ -394,10 +396,7 @@ export default class ConnectionQuality {
394 396
             bitrate: this._localStats.bitrate,
395 397
             packetLoss: this._localStats.packetLoss,
396 398
             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
399
+            jvbRTT: this._localStats.jvbRTT
401 400
         };
402 401
 
403 402
         // TODO: It looks like the remote participants don't really "care"
@@ -429,9 +428,26 @@ export default class ConnectionQuality {
429 428
 
430 429
     /**
431 430
      * Updates the local statistics
431
+     * @param {TraceablePeerConnection} tpc the peerconnection which emitted
432
+     * the stats
432 433
      * @param data new statistics
433 434
      */
434
-    _updateLocalStats(data) {
435
+    _updateLocalStats(tpc, data) {
436
+        // Update jvbRTT
437
+        if (!tpc.isP2P) {
438
+            const jvbRTT
439
+                = data.transport
440
+                    && data.transport.length && data.transport[0].rtt;
441
+
442
+            this._localStats.jvbRTT = jvbRTT ? jvbRTT : undefined;
443
+        }
444
+
445
+        // Do not continue with processing of other stats if they do not
446
+        // originate from the active peerconnection
447
+        if (tpc !== this._conference.getActivePeerConnection()) {
448
+            return;
449
+        }
450
+
435 451
         let key;
436 452
         const updateLocalConnectionQuality
437 453
             = !this._conference.isConnectionInterrupted();

+ 352
- 118
modules/statistics/AvgRTPStatsReporter.js View File

@@ -4,8 +4,10 @@ import { getLogger } from 'jitsi-meet-logger';
4 4
 import * as ConnectionQualityEvents
5 5
     from '../../service/connectivity/ConnectionQualityEvents';
6 6
 import * as ConferenceEvents from '../../JitsiConferenceEvents';
7
+import * as MediaType from '../../service/RTC/MediaType';
7 8
 import RTCBrowserType from '../RTC/RTCBrowserType';
8 9
 import Statistics from './statistics';
10
+import * as VideoType from '../../service/RTC/VideoType';
9 11
 
10 12
 const logger = getLogger(__filename);
11 13
 
@@ -35,11 +37,10 @@ class AverageStatReport {
35 37
             logger.error(
36 38
                 `${this.name} - invalid value for idx: ${this.count}`,
37 39
                 nextValue);
38
-
39
-            return;
40
+        } else if (!isNaN(nextValue)) {
41
+            this.sum += nextValue;
42
+            this.count += 1;
40 43
         }
41
-        this.sum += nextValue;
42
-        this.count += 1;
43 44
     }
44 45
 
45 46
     /**
@@ -74,6 +75,203 @@ class AverageStatReport {
74 75
     }
75 76
 }
76 77
 
78
+/**
79
+ * Class gathers the stats that are calculated and reported for a
80
+ * {@link TraceablePeerConnection} even if it's not currently active. For
81
+ * example we want to monitor RTT for the JVB connection while in P2P mode.
82
+ */
83
+class ConnectionAvgStats {
84
+    /**
85
+     * Creates new <tt>ConnectionAvgStats</tt>
86
+     * @param {JitsiConference} conference
87
+     * @param {boolean} isP2P
88
+     * @param {number} n the number of samples, before arithmetic mean is to be
89
+     * calculated and values submitted to the analytics module.
90
+     */
91
+    constructor(conference, isP2P, n) {
92
+        /**
93
+         * Is this instance for JVB or P2P connection ?
94
+         * @type {boolean}
95
+         */
96
+        this.isP2P = isP2P;
97
+
98
+        /**
99
+         * How many samples are to be included in arithmetic mean calculation.
100
+         * @type {number}
101
+         * @private
102
+         */
103
+        this._n = n;
104
+
105
+        /**
106
+         * The current sample index. Starts from 0 and goes up to {@link _n})
107
+         * when analytics report will be submitted.
108
+         * @type {number}
109
+         * @private
110
+         */
111
+        this._sampleIdx = 0;
112
+
113
+        /**
114
+         * Average round trip time reported by the ICE candidate pair.
115
+         * @type {AverageStatReport}
116
+         */
117
+        this._avgRTT = new AverageStatReport('stat.avg.rtt');
118
+
119
+        /**
120
+         * Map stores average RTT to the JVB reported by remote participants.
121
+         * Mapped per participant id {@link JitsiParticipant.getId}.
122
+         *
123
+         * This is used only when {@link ConnectionAvgStats.isP2P} equals to
124
+         * <tt>false</tt>.
125
+         *
126
+         * @type {Map<string,AverageStatReport>}
127
+         * @private
128
+         */
129
+        this._avgRemoteRTTMap = new Map();
130
+
131
+        /**
132
+         * The conference for which stats will be collected and reported.
133
+         * @type {JitsiConference}
134
+         * @private
135
+         */
136
+        this._conference = conference;
137
+
138
+        this._onConnectionStats = (tpc, stats) => {
139
+            if (this.isP2P === tpc.isP2P) {
140
+                this._calculateAvgStats(stats);
141
+            }
142
+        };
143
+        conference.statistics.addConnectionStatsListener(
144
+            this._onConnectionStats);
145
+
146
+        if (!this.isP2P) {
147
+            this._onUserLeft = id => this._avgRemoteRTTMap.delete(id);
148
+            conference.on(ConferenceEvents.USER_LEFT, this._onUserLeft);
149
+
150
+            this._onRemoteStatsUpdated
151
+                = (id, data) => this._processRemoteStats(id, data);
152
+            conference.on(
153
+                ConnectionQualityEvents.REMOTE_STATS_UPDATED,
154
+                this._onRemoteStatsUpdated);
155
+        }
156
+    }
157
+
158
+    /**
159
+     * Processes next batch of stats.
160
+     * @param {go figure} data
161
+     * @private
162
+     */
163
+    _calculateAvgStats(data) {
164
+        if (!data) {
165
+            logger.error('No stats');
166
+
167
+            return;
168
+        }
169
+
170
+        if (RTCBrowserType.supportsRTTStatistics()) {
171
+            if (data.transport && data.transport.length) {
172
+                this._avgRTT.addNext(data.transport[0].rtt);
173
+            }
174
+        }
175
+
176
+        this._sampleIdx += 1;
177
+
178
+        if (this._sampleIdx >= this._n) {
179
+            if (RTCBrowserType.supportsRTTStatistics()) {
180
+                this._avgRTT.report(this.isP2P);
181
+
182
+                // Report end to end RTT only for JVB
183
+                if (!this.isP2P) {
184
+                    const avgRemoteRTT = this._calculateAvgRemoteRTT();
185
+                    const avgLocalRTT = this._avgRTT.calculate();
186
+
187
+                    if (!isNaN(avgLocalRTT) && !isNaN(avgRemoteRTT)) {
188
+                        Statistics.analytics.sendEvent(
189
+                            'stat.avg.end2endrtt',
190
+                            { value: avgLocalRTT + avgRemoteRTT });
191
+                    }
192
+                }
193
+            }
194
+
195
+            this._resetAvgStats();
196
+        }
197
+    }
198
+
199
+    /**
200
+     * Calculates arithmetic mean of all RTTs towards the JVB reported by
201
+     * participants.
202
+     * @return {number|NaN} NaN if not available (not enough data)
203
+     * @private
204
+     */
205
+    _calculateAvgRemoteRTT() {
206
+        let count = 0, sum = 0;
207
+
208
+        // FIXME should we ignore RTT for participant
209
+        // who "is having connectivity issues" ?
210
+        for (const remoteAvg of this._avgRemoteRTTMap.values()) {
211
+            const avg = remoteAvg.calculate();
212
+
213
+            if (!isNaN(avg)) {
214
+                sum += avg;
215
+                count += 1;
216
+                remoteAvg.reset();
217
+            }
218
+        }
219
+
220
+        return sum / count;
221
+    }
222
+
223
+    /**
224
+     * Processes {@link ConnectionQualityEvents.REMOTE_STATS_UPDATED} to analyse
225
+     * RTT towards the JVB reported by each participant.
226
+     * @param {string} id {@link JitsiParticipant.getId}
227
+     * @param {go figure in ConnectionQuality.js} data
228
+     * @private
229
+     */
230
+    _processRemoteStats(id, data) {
231
+        const validData = typeof data.jvbRTT === 'number';
232
+        let rttAvg = this._avgRemoteRTTMap.get(id);
233
+
234
+        if (!rttAvg && validData) {
235
+            rttAvg = new AverageStatReport(`${id}.stat.rtt`);
236
+            this._avgRemoteRTTMap.set(id, rttAvg);
237
+        }
238
+
239
+        if (validData) {
240
+            rttAvg.addNext(data.jvbRTT);
241
+        } else if (rttAvg) {
242
+            this._avgRemoteRTTMap.delete(id);
243
+        }
244
+    }
245
+
246
+    /**
247
+     * Reset cache of all averages and {@link _sampleIdx}.
248
+     * @private
249
+     */
250
+    _resetAvgStats() {
251
+        this._avgRTT.reset();
252
+        if (this._avgRemoteRTTMap) {
253
+            this._avgRemoteRTTMap.clear();
254
+        }
255
+        this._sampleIdx = 0;
256
+    }
257
+
258
+    /**
259
+     *
260
+     */
261
+    dispose() {
262
+        this._conference.statistics.removeConnectionStatsListener(
263
+            this._onConnectionStats);
264
+        if (!this.isP2P) {
265
+            this._conference.off(
266
+                ConnectionQualityEvents.REMOTE_STATS_UPDATED,
267
+                this._onRemoteStatsUpdated);
268
+            this._conference.off(
269
+                ConferenceEvents.USER_LEFT,
270
+                this._onUserLeft);
271
+        }
272
+    }
273
+}
274
+
77 275
 /**
78 276
  * Reports average RTP statistics values (arithmetic mean) to the analytics
79 277
  * module for things like bit rate, bandwidth, packet loss etc. It keeps track
@@ -123,19 +321,36 @@ export default class AvgRTPStatsReporter {
123 321
         this._conference = conference;
124 322
 
125 323
         /**
126
-         * Average upload bitrate
324
+         * Average audio upload bitrate
325
+         * @type {AverageStatReport}
326
+         * @private
327
+         */
328
+        this._avgAudioBitrateUp
329
+            = new AverageStatReport('stat.avg.bitrate.audio.upload');
330
+
331
+        /**
332
+         * Average audio download bitrate
333
+         * @type {AverageStatReport}
334
+         * @private
335
+         */
336
+        this._avgAudioBitrateDown
337
+            = new AverageStatReport('stat.avg.bitrate.audio.download');
338
+
339
+        /**
340
+         * Average video upload bitrate
127 341
          * @type {AverageStatReport}
128 342
          * @private
129 343
          */
130
-        this._avgBitrateUp = new AverageStatReport('stat.avg.bitrate.upload');
344
+        this._avgVideoBitrateUp
345
+            = new AverageStatReport('stat.avg.bitrate.video.upload');
131 346
 
132 347
         /**
133
-         * Average download bitrate
348
+         * Average video download bitrate
134 349
          * @type {AverageStatReport}
135 350
          * @private
136 351
          */
137
-        this._avgBitrateDown
138
-            = new AverageStatReport('stat.avg.bitrate.download');
352
+        this._avgVideoBitrateDown
353
+            = new AverageStatReport('stat.avg.bitrate.video.download');
139 354
 
140 355
         /**
141 356
          * Average upload bandwidth
@@ -185,27 +400,29 @@ export default class AvgRTPStatsReporter {
185 400
         this._avgRemoteFPS = new AverageStatReport('stat.avg.framerate.remote');
186 401
 
187 402
         /**
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>}
403
+         * Average FPS for remote screen streaming videos (reported only if not
404
+         * a <tt>NaN</tt>).
405
+         * @type {AverageStatReport}
191 406
          * @private
192 407
          */
193
-        this._avgRemoteRTTMap = new Map();
408
+        this._avgRemoteScreenFPS
409
+            = new AverageStatReport('stat.avg.framerate.screen.remote');
194 410
 
195 411
         /**
196
-         * Average round trip time reported by the ICE candidate pair.
197
-         * FIXME currently reported only for P2P
412
+         * Average FPS for local video (camera)
198 413
          * @type {AverageStatReport}
199 414
          * @private
200 415
          */
201
-        this._avgRTT = new AverageStatReport('stat.avg.rtt');
416
+        this._avgLocalFPS = new AverageStatReport('stat.avg.framerate.local');
202 417
 
203 418
         /**
204
-         * Average FPS for local video
419
+         * Average FPS for local screen streaming video (reported only if not
420
+         * a <tt>NaN</tt>).
205 421
          * @type {AverageStatReport}
206 422
          * @private
207 423
          */
208
-        this._avgLocalFPS = new AverageStatReport('stat.avg.framerate.local');
424
+        this._avgLocalScreenFPS
425
+            = new AverageStatReport('stat.avg.framerate.screen.local');
209 426
 
210 427
         /**
211 428
          * Average connection quality as defined by
@@ -220,46 +437,21 @@ export default class AvgRTPStatsReporter {
220 437
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
221 438
             this._onLocalStatsUpdated);
222 439
 
223
-        this._onRemoteStatsUpdated
224
-            = (id, data) => this._processRemoteStats(id, data);
225
-        conference.on(
226
-            ConnectionQualityEvents.REMOTE_STATS_UPDATED,
227
-            this._onRemoteStatsUpdated);
228
-
229 440
         this._onP2PStatusChanged = () => {
230 441
             logger.debug('Resetting average stats calculation');
231 442
             this._resetAvgStats();
443
+            this.jvbStatsMonitor._resetAvgStats();
444
+            this.p2pStatsMonitor._resetAvgStats();
232 445
         };
233 446
         conference.on(
234 447
             ConferenceEvents.P2P_STATUS,
235 448
             this._onP2PStatusChanged);
236 449
 
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
-        }
450
+        this.jvbStatsMonitor
451
+            = new ConnectionAvgStats(conference, false /* JVB */, n);
261 452
 
262
-        return sum / count;
453
+        this.p2pStatsMonitor
454
+            = new ConnectionAvgStats(conference, true /* P2P */, n);
263 455
     }
264 456
 
265 457
     /**
@@ -317,8 +509,11 @@ export default class AvgRTPStatsReporter {
317 509
             return;
318 510
         }
319 511
 
320
-        this._avgBitrateUp.addNext(bitrate.upload);
321
-        this._avgBitrateDown.addNext(bitrate.download);
512
+        this._avgAudioBitrateUp.addNext(bitrate.audio.upload);
513
+        this._avgAudioBitrateDown.addNext(bitrate.audio.download);
514
+
515
+        this._avgVideoBitrateUp.addNext(bitrate.video.upload);
516
+        this._avgVideoBitrateDown.addNext(bitrate.video.download);
322 517
 
323 518
         if (RTCBrowserType.supportsBandwidthStatistics()) {
324 519
             this._avgBandwidthUp.addNext(bandwidth.upload);
@@ -328,28 +523,34 @@ export default class AvgRTPStatsReporter {
328 523
         this._avgPacketLossUp.addNext(packetLoss.upload);
329 524
         this._avgPacketLossDown.addNext(packetLoss.download);
330 525
         this._avgPacketLossTotal.addNext(packetLoss.total);
331
-        this._avgCQ.addNext(data.connectionQuality);
332 526
 
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
-        }
527
+        this._avgCQ.addNext(data.connectionQuality);
340 528
 
341 529
         if (frameRate) {
342 530
             this._avgRemoteFPS.addNext(
343
-                this._calculateAvgVideoFps(frameRate, false /* remote */));
531
+                this._calculateAvgVideoFps(
532
+                    frameRate, false /* remote */, VideoType.CAMERA));
533
+            this._avgRemoteScreenFPS.addNext(
534
+                this._calculateAvgVideoFps(
535
+                    frameRate, false /* remote */, VideoType.DESKTOP));
536
+
344 537
             this._avgLocalFPS.addNext(
345
-                this._calculateAvgVideoFps(frameRate, true /* local */));
538
+                this._calculateAvgVideoFps(
539
+                    frameRate, true /* local */, VideoType.CAMERA));
540
+            this._avgLocalScreenFPS.addNext(
541
+                this._calculateAvgVideoFps(
542
+                    frameRate, true /* local */, VideoType.DESKTOP));
346 543
         }
347 544
 
348 545
         this._sampleIdx += 1;
349 546
 
350 547
         if (this._sampleIdx >= this._n) {
351
-            this._avgBitrateUp.report(isP2P);
352
-            this._avgBitrateDown.report(isP2P);
548
+            this._avgAudioBitrateUp.report(isP2P);
549
+            this._avgAudioBitrateDown.report(isP2P);
550
+
551
+            this._avgVideoBitrateUp.report(isP2P);
552
+            this._avgVideoBitrateDown.report(isP2P);
553
+
353 554
             if (RTCBrowserType.supportsBandwidthStatistics()) {
354 555
                 this._avgBandwidthUp.report(isP2P);
355 556
                 this._avgBandwidthDown.report(isP2P);
@@ -357,24 +558,18 @@ export default class AvgRTPStatsReporter {
357 558
             this._avgPacketLossUp.report(isP2P);
358 559
             this._avgPacketLossDown.report(isP2P);
359 560
             this._avgPacketLossTotal.report(isP2P);
561
+
360 562
             this._avgRemoteFPS.report(isP2P);
563
+            if (!isNaN(this._avgRemoteScreenFPS.calculate())) {
564
+                this._avgRemoteScreenFPS.report(isP2P);
565
+            }
361 566
             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
-                }
567
+            if (!isNaN(this._avgLocalScreenFPS.calculate())) {
568
+                this._avgLocalScreenFPS.report(isP2P);
376 569
             }
377 570
 
571
+            this._avgCQ.report(isP2P);
572
+
378 573
             this._resetAvgStats();
379 574
         }
380 575
     }
@@ -384,58 +579,92 @@ export default class AvgRTPStatsReporter {
384 579
      * @param {go figure} frameRate
385 580
      * @param {boolean} isLocal if the average is to be calculated for the local
386 581
      * video or <tt>false</tt> if for remote videos.
582
+     * @param {VideoType} videoType
387 583
      * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
388 584
      * @private
389 585
      */
390
-    _calculateAvgVideoFps(frameRate, isLocal) {
586
+    _calculateAvgVideoFps(frameRate, isLocal, videoType) {
587
+        let peerFpsSum = 0;
391 588
         let peerCount = 0;
392
-        let subFrameAvg = 0;
393 589
         const myID = this._conference.myUserId();
394 590
 
395 591
         for (const peerID of Object.keys(frameRate)) {
396 592
             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);
593
+                const participant
594
+                    = isLocal
595
+                        ? null : this._conference.getParticipantById(peerID);
596
+                const videosFps = frameRate[peerID];
597
+
598
+                // Do not continue without participant for non local peerID
599
+                if ((isLocal || participant) && videosFps) {
600
+                    const peerAvgFPS
601
+                        = this._calculatePeerAvgVideoFps(
602
+                            videosFps, participant, videoType);
603
+
604
+                    if (!isNaN(peerAvgFPS)) {
605
+                        peerFpsSum += peerAvgFPS;
606
+                        peerCount += 1;
405 607
                     }
406
-
407
-                    peerAvg /= ssrcs.length;
408
-
409
-                    subFrameAvg += peerAvg;
410
-                    peerCount += 1;
411 608
                 }
412 609
             }
413 610
         }
414 611
 
415
-        return subFrameAvg / peerCount;
612
+        return peerFpsSum / peerCount;
416 613
     }
417 614
 
418 615
     /**
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
616
+     * Calculate average FPS for either remote or local participant
617
+     * @param {object} videos maps FPS per video SSRC
618
+     * @param {JitsiParticipant|null} participant remote participant or
619
+     * <tt>null</tt> for local FPS calculation.
620
+     * @param {VideoType} videoType the type of the video for which an average
621
+     * will be calculated.
622
+     * @return {number|NaN} average FPS of all participant's videos or
623
+     * <tt>NaN</tt> if currently not available
423 624
      * @private
424 625
      */
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);
626
+    _calculatePeerAvgVideoFps(videos, participant, videoType) {
627
+        let ssrcs = Object.keys(videos).map(ssrc => Number(ssrc));
628
+        let videoTracks = null;
629
+
630
+        // NOTE that this method is supposed to be called for the stats
631
+        // received from the current peerconnection.
632
+        const tpc = this._conference.getActivePeerConnection();
633
+
634
+        if (participant) {
635
+            videoTracks = participant.getTracksByMediaType(MediaType.VIDEO);
636
+            if (videoTracks) {
637
+                ssrcs
638
+                    = ssrcs.filter(
639
+                        ssrc => videoTracks.find(
640
+                            track => !track.isMuted()
641
+                                && track.getSSRC() === ssrc
642
+                                && track.videoType === videoType));
643
+            }
644
+        } else {
645
+            videoTracks = this._conference.getLocalTracks(MediaType.VIDEO);
646
+            ssrcs
647
+                = ssrcs.filter(
648
+                    ssrc => videoTracks.find(
649
+                        track => !track.isMuted()
650
+                            && tpc.getLocalSSRC(track) === ssrc
651
+                            && track.videoType === videoType));
432 652
         }
433 653
 
434
-        if (validData) {
435
-            rttAvg.addNext(data.jvbRTT);
436
-        } else if (rttAvg) {
437
-            this._avgRemoteRTTMap.delete(id);
654
+        let peerFpsSum = 0;
655
+        let peerSsrcCount = 0;
656
+
657
+        for (const ssrc of ssrcs) {
658
+            const peerSsrcFps = Number(videos[ssrc]);
659
+
660
+            // FPS is reported as 0 for users with no video
661
+            if (!isNaN(peerSsrcFps) && peerSsrcFps > 0) {
662
+                peerFpsSum += peerSsrcFps;
663
+                peerSsrcCount += 1;
664
+            }
438 665
         }
666
+
667
+        return peerFpsSum / peerSsrcCount;
439 668
     }
440 669
 
441 670
     /**
@@ -443,17 +672,26 @@ export default class AvgRTPStatsReporter {
443 672
      * @private
444 673
      */
445 674
     _resetAvgStats() {
446
-        this._avgBitrateUp.reset();
447
-        this._avgBitrateDown.reset();
675
+        this._avgAudioBitrateUp.reset();
676
+        this._avgAudioBitrateDown.reset();
677
+
678
+        this._avgVideoBitrateUp.reset();
679
+        this._avgVideoBitrateDown.reset();
680
+
448 681
         this._avgBandwidthUp.reset();
449 682
         this._avgBandwidthDown.reset();
683
+
450 684
         this._avgPacketLossUp.reset();
451 685
         this._avgPacketLossDown.reset();
686
+        this._avgPacketLossTotal.reset();
687
+
452 688
         this._avgRemoteFPS.reset();
689
+        this._avgRemoteScreenFPS.reset();
453 690
         this._avgLocalFPS.reset();
691
+        this._avgLocalScreenFPS.reset();
692
+
454 693
         this._avgCQ.reset();
455
-        this._avgRTT.reset();
456
-        this._avgRemoteRTTMap.clear();
694
+
457 695
         this._sampleIdx = 0;
458 696
     }
459 697
 
@@ -467,11 +705,7 @@ export default class AvgRTPStatsReporter {
467 705
         this._conference.off(
468 706
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
469 707
             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);
708
+        this.jvbStatsMonitor.dispose();
709
+        this.p2pStatsMonitor.dispose();
476 710
     }
477 711
 }

+ 84
- 19
modules/statistics/RTPStatsCollector.js View File

@@ -664,8 +664,12 @@ StatsCollector.prototype.processStatsReport = function() {
664 664
     };
665 665
     let bitrateDownload = 0;
666 666
     let bitrateUpload = 0;
667
-    const resolutions = new Map();
668
-    const framerates = new Map();
667
+    const resolutions = {};
668
+    const framerates = {};
669
+    let audioBitrateDownload = 0;
670
+    let audioBitrateUpload = 0;
671
+    let videoBitrateDownload = 0;
672
+    let videoBitrateUpload = 0;
669 673
 
670 674
     for (const [ ssrc, ssrcStats ] of this.ssrc2stats) {
671 675
         // process packet loss stats
@@ -679,21 +683,75 @@ StatsCollector.prototype.processStatsReport = function() {
679 683
         bitrateDownload += ssrcStats.bitrate.download;
680 684
         bitrateUpload += ssrcStats.bitrate.upload;
681 685
 
682
-        ssrcStats.resetBitrate();
686
+        // collect resolutions and framerates
687
+        const track = this.peerconnection.getTrackBySSRC(ssrc);
688
+
689
+        if (track) {
690
+            if (track.isAudioTrack()) {
691
+                audioBitrateDownload += ssrcStats.bitrate.download;
692
+                audioBitrateUpload += ssrcStats.bitrate.upload;
693
+            } else {
694
+                videoBitrateDownload += ssrcStats.bitrate.download;
695
+                videoBitrateUpload += ssrcStats.bitrate.upload;
696
+            }
697
+
698
+            const participantId = track.getParticipantId();
699
+
700
+            if (participantId) {
701
+                const resolution = ssrcStats.resolution;
683 702
 
684
-        // collect resolutions
685
-        resolutions.set(ssrc, ssrcStats.resolution);
703
+                if (resolution.width
704
+                        && resolution.height
705
+                        && resolution.width !== -1
706
+                        && resolution.height !== -1) {
707
+                    const userResolutions = resolutions[participantId] || {};
686 708
 
687
-        // collect framerates
688
-        framerates.set(ssrc, ssrcStats.framerate);
709
+                    userResolutions[ssrc] = resolution;
710
+                    resolutions[participantId] = userResolutions;
711
+                }
712
+                if (ssrcStats.framerate !== 0) {
713
+                    const userFramerates = framerates[participantId] || {};
714
+
715
+                    userFramerates[ssrc] = ssrcStats.framerate;
716
+                    framerates[participantId] = userFramerates;
717
+                }
718
+            } else {
719
+                logger.error(`No participant ID returned by ${track}`);
720
+            }
721
+        } else if (this.peerconnection.isP2P) {
722
+            // NOTE For JVB connection there are JVB tracks reported in
723
+            // the stats, but they do not have corresponding JitsiRemoteTrack
724
+            // instances stored in TPC. It is not trivial to figure out that
725
+            // a SSRC belongs to JVB, so we print this error ony for the P2P
726
+            // connection for the time being.
727
+            //
728
+            // Also there will be reports for tracks removed from the session,
729
+            // for the users who have left the conference.
730
+            logger.error(
731
+                `JitsiTrack not found for SSRC ${ssrc}`
732
+                    + ` in ${this.peerconnection}`);
733
+        }
734
+
735
+        ssrcStats.resetBitrate();
689 736
     }
690 737
 
691 738
     this.eventEmitter.emit(
692 739
         StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
693 740
 
694
-    this.conferenceStats.bitrate
695
-      = { 'upload': bitrateUpload,
696
-          'download': bitrateDownload };
741
+    this.conferenceStats.bitrate = {
742
+        'upload': bitrateUpload,
743
+        'download': bitrateDownload
744
+    };
745
+
746
+    this.conferenceStats.bitrate.audio = {
747
+        'upload': audioBitrateUpload,
748
+        'download': audioBitrateDownload
749
+    };
750
+
751
+    this.conferenceStats.bitrate.video = {
752
+        'upload': videoBitrateUpload,
753
+        'download': videoBitrateDownload
754
+    };
697 755
 
698 756
     this.conferenceStats.packetLoss = {
699 757
         total:
@@ -704,14 +762,17 @@ StatsCollector.prototype.processStatsReport = function() {
704 762
         upload:
705 763
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
706 764
     };
707
-    this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS, {
708
-        'bandwidth': this.conferenceStats.bandwidth,
709
-        'bitrate': this.conferenceStats.bitrate,
710
-        'packetLoss': this.conferenceStats.packetLoss,
711
-        'resolution': resolutions,
712
-        'framerate': framerates,
713
-        'transport': this.conferenceStats.transport
714
-    });
765
+    this.eventEmitter.emit(
766
+        StatisticsEvents.CONNECTION_STATS,
767
+        this.peerconnection,
768
+        {
769
+            'bandwidth': this.conferenceStats.bandwidth,
770
+            'bitrate': this.conferenceStats.bitrate,
771
+            'packetLoss': this.conferenceStats.packetLoss,
772
+            'resolution': resolutions,
773
+            'framerate': framerates,
774
+            'transport': this.conferenceStats.transport
775
+        });
715 776
     this.conferenceStats.transport = [];
716 777
 };
717 778
 
@@ -772,7 +833,11 @@ StatsCollector.prototype.processAudioLevelReport = function() {
772 833
             // seems to vary between 0 and around 32k.
773 834
             audioLevel = audioLevel / 32767;
774 835
             this.eventEmitter.emit(
775
-                StatisticsEvents.AUDIO_LEVEL, ssrc, audioLevel, isLocal);
836
+                StatisticsEvents.AUDIO_LEVEL,
837
+                this.peerconnection,
838
+                ssrc,
839
+                audioLevel,
840
+                isLocal);
776 841
         }
777 842
     }
778 843
 };

+ 36
- 11
modules/statistics/statistics.js View File

@@ -95,7 +95,12 @@ Statistics.init = function(options) {
95 95
  * @param options
96 96
  */
97 97
 export default function Statistics(xmpp, options) {
98
-    this.rtpStats = null;
98
+    /**
99
+     * {@link RTPStats} mapped by {@link TraceablePeerConnection.id} which
100
+     * collect RTP statistics for each peerconnection.
101
+     * @type {Map<string, RTPStats}
102
+     */
103
+    this.rtpStatsMap = new Map();
99 104
     this.eventEmitter = new EventEmitter();
100 105
     this.xmpp = xmpp;
101 106
     this.options = options || {};
@@ -145,16 +150,21 @@ Object.defineProperty(Statistics, 'instances', {
145 150
     }
146 151
 });
147 152
 
153
+/**
154
+ * Starts collecting RTP stats for given peerconnection.
155
+ * @param {TraceablePeerConnection} peerconnection
156
+ */
148 157
 Statistics.prototype.startRemoteStats = function(peerconnection) {
149
-    this.stopRemoteStats();
158
+    this.stopRemoteStats(peerconnection);
150 159
 
151 160
     try {
152
-        this.rtpStats
161
+        const rtpStats
153 162
             = new RTPStats(peerconnection,
154 163
                     Statistics.audioLevelsInterval, 2000, this.eventEmitter);
155
-        this.rtpStats.start(Statistics.audioLevelsEnabled);
164
+
165
+        rtpStats.start(Statistics.audioLevelsEnabled);
166
+        this.rtpStatsMap.set(peerconnection.id, rtpStats);
156 167
     } catch (e) {
157
-        this.rtpStats = null;
158 168
         logger.error(`Failed to start collecting remote statistics: ${e}`);
159 169
     }
160 170
 };
@@ -229,7 +239,9 @@ Statistics.prototype.dispose = function() {
229 239
         for (const callStats of this.callsStatsInstances.values()) {
230 240
             this.stopCallStats(callStats.tpc);
231 241
         }
232
-        this.stopRemoteStats();
242
+        for (const tpcId of this.rtpStatsMap.keys()) {
243
+            this._stopRemoteStats(tpcId);
244
+        }
233 245
         if (this.eventEmitter) {
234 246
             this.eventEmitter.removeAllListeners();
235 247
         }
@@ -253,13 +265,26 @@ Statistics.stopLocalStats = function(stream) {
253 265
     }
254 266
 };
255 267
 
256
-Statistics.prototype.stopRemoteStats = function() {
257
-    if (!this.rtpStats) {
258
-        return;
268
+/**
269
+ * Stops remote RTP stats for given peerconnection ID.
270
+ * @param {string} tpcId {@link TraceablePeerConnection.id}
271
+ * @private
272
+ */
273
+Statistics.prototype._stopRemoteStats = function(tpcId) {
274
+    const rtpStats = this.rtpStatsMap.get(tpcId);
275
+
276
+    if (rtpStats) {
277
+        rtpStats.stop();
278
+        this.rtpStatsMap.delete(tpcId);
259 279
     }
280
+};
260 281
 
261
-    this.rtpStats.stop();
262
-    this.rtpStats = null;
282
+/**
283
+ * Stops collecting RTP stats for given peerconnection
284
+ * @param {TraceablePeerConnection} tpc
285
+ */
286
+Statistics.prototype.stopRemoteStats = function(tpc) {
287
+    this._stopRemoteStats(tpc.id);
263 288
 };
264 289
 
265 290
 // CALSTATS METHODS

Loading…
Cancel
Save