Ver código fonte

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 anos atrás
pai
commit
9cccf3b62e

+ 31
- 34
JitsiConference.js Ver arquivo

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
  * @param audioLevel the audio level
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
         () => emitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track));
1162
         () => emitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track));
1152
     track.addEventListener(
1163
     track.addEventListener(
1153
         JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
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
         this.statistics.startCallStats(
1345
         this.statistics.startCallStats(
1331
             this.jvbJingleSession.peerconnection,
1346
             this.jvbJingleSession.peerconnection,
1332
             'jitsi' /* Remote user ID for JVB is 'jitsi' */);
1347
             'jitsi' /* Remote user ID for JVB is 'jitsi' */);
1333
-        this._startRemoteStats();
1348
+        this.statistics.startRemoteStats(this.jvbJingleSession.peerconnection);
1334
     } catch (e) {
1349
     } catch (e) {
1335
         GlobalOnErrorHandler.callErrorHandler(e);
1350
         GlobalOnErrorHandler.callErrorHandler(e);
1336
         logger.error(e);
1351
         logger.error(e);
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
  * Handles the call ended event.
1404
  * Handles the call ended event.
1403
  * @param {JingleSessionPC} jingleSession the jingle session which has been
1405
  * @param {JingleSessionPC} jingleSession the jingle session which has been
1419
 
1421
 
1420
         // Stop the stats
1422
         // Stop the stats
1421
         if (this.statistics) {
1423
         if (this.statistics) {
1422
-            this.statistics.stopRemoteStats();
1424
+            this.statistics.stopRemoteStats(
1425
+                this.jvbJingleSession.peerconnection);
1423
             logger.info('Stopping JVB CallStats');
1426
             logger.info('Stopping JVB CallStats');
1424
             this.statistics.stopCallStats(
1427
             this.statistics.stopCallStats(
1425
                 this.jvbJingleSession.peerconnection);
1428
                 this.jvbJingleSession.peerconnection);
1623
  *
1626
  *
1624
  * @return {TraceablePeerConnection|null} null if there isn't any active
1627
  * @return {TraceablePeerConnection|null} null if there isn't any active
1625
  * <tt>TraceablePeerConnection</tt> currently available.
1628
  * <tt>TraceablePeerConnection</tt> currently available.
1626
- * @protected
1629
+ * @public (FIXME how to make package local ?)
1627
  */
1630
  */
1628
 JitsiConference.prototype.getActivePeerConnection = function() {
1631
 JitsiConference.prototype.getActivePeerConnection = function() {
1629
     if (this.isP2PActive()) {
1632
     if (this.isP2PActive()) {
2001
 
2004
 
2002
     // Start remote stats
2005
     // Start remote stats
2003
     logger.info('Starting remote stats with p2p connection');
2006
     logger.info('Starting remote stats with p2p connection');
2004
-    this._startRemoteStats();
2007
+    this.statistics.startRemoteStats(this.p2pJingleSession.peerconnection);
2005
 
2008
 
2006
     // Log the P2P established event
2009
     // Log the P2P established event
2007
     if (this.p2pJingleSession.isInitiator) {
2010
     if (this.p2pJingleSession.isInitiator) {
2276
 
2279
 
2277
     // Stop P2P stats
2280
     // Stop P2P stats
2278
     logger.info('Stopping remote stats for P2P connection');
2281
     logger.info('Stopping remote stats for P2P connection');
2279
-    this.statistics.stopRemoteStats();
2282
+    this.statistics.stopRemoteStats(this.p2pJingleSession.peerconnection);
2280
     logger.info('Stopping CallStats for P2P connection');
2283
     logger.info('Stopping CallStats for P2P connection');
2281
     this.statistics.stopCallStats(
2284
     this.statistics.stopCallStats(
2282
         this.p2pJingleSession.peerconnection);
2285
         this.p2pJingleSession.peerconnection);
2309
             logger.info('Not adding remote JVB tracks - no session yet');
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 Ver arquivo

47
         Statistics.sendEventToAll.bind(Statistics, 'connection.restored'));
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
  * Setups event listeners related to conference.chatRoom
51
  * Setups event listeners related to conference.chatRoom
105
  */
52
  */
621
         return;
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
     // Forward the "before stats disposed" event
578
     // Forward the "before stats disposed" event
629
     conference.statistics.addBeforeDisposedListener(() => {
579
     conference.statistics.addBeforeDisposedListener(() => {
630
         conference.eventEmitter.emit(
580
         conference.eventEmitter.emit(
631
             JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED);
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
     conference.statistics.addByteSentStatsListener((tpc, stats) => {
583
     conference.statistics.addByteSentStatsListener((tpc, stats) => {
643
         conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
584
         conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
644
             const ssrc = tpc.getLocalSSRC(track);
585
             const ssrc = tpc.getLocalSSRC(track);

+ 0
- 6
JitsiConferenceEvents.js Ver arquivo

56
  */
56
  */
57
 export const CONNECTION_RESTORED = 'conference.connectionRestored';
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
  * A user has changed it display name
60
  * A user has changed it display name
67
  */
61
  */

+ 2
- 1
JitsiMeetJS.js Ver arquivo

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

+ 9
- 0
JitsiParticipant.js Ver arquivo

121
         return this._tracks.slice();
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
      * @returns {String} The ID of this participant.
134
      * @returns {String} The ID of this participant.
126
      */
135
      */

+ 10
- 4
modules/RTC/JitsiTrack.js Ver arquivo

381
 
381
 
382
 /**
382
 /**
383
  * Sets the audio level for the stream
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
     if (this.audioLevel !== audioLevel) {
391
     if (this.audioLevel !== audioLevel) {
388
-        this.eventEmitter.emit(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
389
-            audioLevel);
390
         this.audioLevel = audioLevel;
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 Ver arquivo

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
         if (!track) {
661
         if (!track) {
659
             return;
662
             return;
660
-        }
661
-        if (!track.isAudioTrack()) {
663
+        } else if (!track.isAudioTrack()) {
662
             logger.warn(`Received audio level for non-audio track: ${ssrc}`);
664
             logger.warn(`Received audio level for non-audio track: ${ssrc}`);
663
 
665
 
664
             return;
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
      * Sends message via the datachannels.
678
      * Sends message via the datachannels.

+ 24
- 0
modules/RTC/TraceablePeerConnection.js Ver arquivo

473
     return remoteTracks;
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
  * Called when new remote MediaStream is added to the PeerConnection.
501
  * Called when new remote MediaStream is added to the PeerConnection.
478
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant
502
  * @param {MediaStream} stream the WebRTC MediaStream for remote participant

+ 24
- 8
modules/connectivity/ConnectionQuality.js Ver arquivo

161
         /**
161
         /**
162
          * Holds statistics about the local connection quality.
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
          * The time this._localStats.connectionQuality was last updated.
170
          * The time this._localStats.connectionQuality was last updated.
232
         // jitsi-meet
235
         // jitsi-meet
233
         // TODO: We should keep track of the remote resolution in _remoteStats,
236
         // TODO: We should keep track of the remote resolution in _remoteStats,
234
         // and notify about changes via separate events.
237
         // and notify about changes via separate events.
235
-        conference.on(
236
-            ConferenceEvents.CONNECTION_STATS,
238
+        conference.statistics.addConnectionStatsListener(
237
             this._updateLocalStats.bind(this));
239
             this._updateLocalStats.bind(this));
238
 
240
 
239
         // Save the last time we were unmuted.
241
         // Save the last time we were unmuted.
394
             bitrate: this._localStats.bitrate,
396
             bitrate: this._localStats.bitrate,
395
             packetLoss: this._localStats.packetLoss,
397
             packetLoss: this._localStats.packetLoss,
396
             connectionQuality: this._localStats.connectionQuality,
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
         // TODO: It looks like the remote participants don't really "care"
402
         // TODO: It looks like the remote participants don't really "care"
429
 
428
 
430
     /**
429
     /**
431
      * Updates the local statistics
430
      * Updates the local statistics
431
+     * @param {TraceablePeerConnection} tpc the peerconnection which emitted
432
+     * the stats
432
      * @param data new statistics
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
         let key;
451
         let key;
436
         const updateLocalConnectionQuality
452
         const updateLocalConnectionQuality
437
             = !this._conference.isConnectionInterrupted();
453
             = !this._conference.isConnectionInterrupted();

+ 352
- 118
modules/statistics/AvgRTPStatsReporter.js Ver arquivo

4
 import * as ConnectionQualityEvents
4
 import * as ConnectionQualityEvents
5
     from '../../service/connectivity/ConnectionQualityEvents';
5
     from '../../service/connectivity/ConnectionQualityEvents';
6
 import * as ConferenceEvents from '../../JitsiConferenceEvents';
6
 import * as ConferenceEvents from '../../JitsiConferenceEvents';
7
+import * as MediaType from '../../service/RTC/MediaType';
7
 import RTCBrowserType from '../RTC/RTCBrowserType';
8
 import RTCBrowserType from '../RTC/RTCBrowserType';
8
 import Statistics from './statistics';
9
 import Statistics from './statistics';
10
+import * as VideoType from '../../service/RTC/VideoType';
9
 
11
 
10
 const logger = getLogger(__filename);
12
 const logger = getLogger(__filename);
11
 
13
 
35
             logger.error(
37
             logger.error(
36
                 `${this.name} - invalid value for idx: ${this.count}`,
38
                 `${this.name} - invalid value for idx: ${this.count}`,
37
                 nextValue);
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
     }
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
  * Reports average RTP statistics values (arithmetic mean) to the analytics
276
  * Reports average RTP statistics values (arithmetic mean) to the analytics
79
  * module for things like bit rate, bandwidth, packet loss etc. It keeps track
277
  * module for things like bit rate, bandwidth, packet loss etc. It keeps track
123
         this._conference = conference;
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
          * @type {AverageStatReport}
341
          * @type {AverageStatReport}
128
          * @private
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
          * @type {AverageStatReport}
349
          * @type {AverageStatReport}
135
          * @private
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
          * Average upload bandwidth
356
          * Average upload bandwidth
185
         this._avgRemoteFPS = new AverageStatReport('stat.avg.framerate.remote');
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
          * @private
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
          * @type {AverageStatReport}
413
          * @type {AverageStatReport}
199
          * @private
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
          * @type {AverageStatReport}
421
          * @type {AverageStatReport}
206
          * @private
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
          * Average connection quality as defined by
428
          * Average connection quality as defined by
220
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
437
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
221
             this._onLocalStatsUpdated);
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
         this._onP2PStatusChanged = () => {
440
         this._onP2PStatusChanged = () => {
230
             logger.debug('Resetting average stats calculation');
441
             logger.debug('Resetting average stats calculation');
231
             this._resetAvgStats();
442
             this._resetAvgStats();
443
+            this.jvbStatsMonitor._resetAvgStats();
444
+            this.p2pStatsMonitor._resetAvgStats();
232
         };
445
         };
233
         conference.on(
446
         conference.on(
234
             ConferenceEvents.P2P_STATUS,
447
             ConferenceEvents.P2P_STATUS,
235
             this._onP2PStatusChanged);
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
             return;
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
         if (RTCBrowserType.supportsBandwidthStatistics()) {
518
         if (RTCBrowserType.supportsBandwidthStatistics()) {
324
             this._avgBandwidthUp.addNext(bandwidth.upload);
519
             this._avgBandwidthUp.addNext(bandwidth.upload);
328
         this._avgPacketLossUp.addNext(packetLoss.upload);
523
         this._avgPacketLossUp.addNext(packetLoss.upload);
329
         this._avgPacketLossDown.addNext(packetLoss.download);
524
         this._avgPacketLossDown.addNext(packetLoss.download);
330
         this._avgPacketLossTotal.addNext(packetLoss.total);
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
         if (frameRate) {
529
         if (frameRate) {
342
             this._avgRemoteFPS.addNext(
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
             this._avgLocalFPS.addNext(
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
         this._sampleIdx += 1;
545
         this._sampleIdx += 1;
349
 
546
 
350
         if (this._sampleIdx >= this._n) {
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
             if (RTCBrowserType.supportsBandwidthStatistics()) {
554
             if (RTCBrowserType.supportsBandwidthStatistics()) {
354
                 this._avgBandwidthUp.report(isP2P);
555
                 this._avgBandwidthUp.report(isP2P);
355
                 this._avgBandwidthDown.report(isP2P);
556
                 this._avgBandwidthDown.report(isP2P);
357
             this._avgPacketLossUp.report(isP2P);
558
             this._avgPacketLossUp.report(isP2P);
358
             this._avgPacketLossDown.report(isP2P);
559
             this._avgPacketLossDown.report(isP2P);
359
             this._avgPacketLossTotal.report(isP2P);
560
             this._avgPacketLossTotal.report(isP2P);
561
+
360
             this._avgRemoteFPS.report(isP2P);
562
             this._avgRemoteFPS.report(isP2P);
563
+            if (!isNaN(this._avgRemoteScreenFPS.calculate())) {
564
+                this._avgRemoteScreenFPS.report(isP2P);
565
+            }
361
             this._avgLocalFPS.report(isP2P);
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
             this._resetAvgStats();
573
             this._resetAvgStats();
379
         }
574
         }
380
     }
575
     }
384
      * @param {go figure} frameRate
579
      * @param {go figure} frameRate
385
      * @param {boolean} isLocal if the average is to be calculated for the local
580
      * @param {boolean} isLocal if the average is to be calculated for the local
386
      * video or <tt>false</tt> if for remote videos.
581
      * video or <tt>false</tt> if for remote videos.
582
+     * @param {VideoType} videoType
387
      * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
583
      * @return {number|NaN} average FPS or <tt>NaN</tt> if there are no samples.
388
      * @private
584
      * @private
389
      */
585
      */
390
-    _calculateAvgVideoFps(frameRate, isLocal) {
586
+    _calculateAvgVideoFps(frameRate, isLocal, videoType) {
587
+        let peerFpsSum = 0;
391
         let peerCount = 0;
588
         let peerCount = 0;
392
-        let subFrameAvg = 0;
393
         const myID = this._conference.myUserId();
589
         const myID = this._conference.myUserId();
394
 
590
 
395
         for (const peerID of Object.keys(frameRate)) {
591
         for (const peerID of Object.keys(frameRate)) {
396
             if (isLocal ? peerID === myID : peerID !== myID) {
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
      * @private
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
      * @private
672
      * @private
444
      */
673
      */
445
     _resetAvgStats() {
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
         this._avgBandwidthUp.reset();
681
         this._avgBandwidthUp.reset();
449
         this._avgBandwidthDown.reset();
682
         this._avgBandwidthDown.reset();
683
+
450
         this._avgPacketLossUp.reset();
684
         this._avgPacketLossUp.reset();
451
         this._avgPacketLossDown.reset();
685
         this._avgPacketLossDown.reset();
686
+        this._avgPacketLossTotal.reset();
687
+
452
         this._avgRemoteFPS.reset();
688
         this._avgRemoteFPS.reset();
689
+        this._avgRemoteScreenFPS.reset();
453
         this._avgLocalFPS.reset();
690
         this._avgLocalFPS.reset();
691
+        this._avgLocalScreenFPS.reset();
692
+
454
         this._avgCQ.reset();
693
         this._avgCQ.reset();
455
-        this._avgRTT.reset();
456
-        this._avgRemoteRTTMap.clear();
694
+
457
         this._sampleIdx = 0;
695
         this._sampleIdx = 0;
458
     }
696
     }
459
 
697
 
467
         this._conference.off(
705
         this._conference.off(
468
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
706
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
469
             this._onLocalStatsUpdated);
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 Ver arquivo

664
     };
664
     };
665
     let bitrateDownload = 0;
665
     let bitrateDownload = 0;
666
     let bitrateUpload = 0;
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
     for (const [ ssrc, ssrcStats ] of this.ssrc2stats) {
674
     for (const [ ssrc, ssrcStats ] of this.ssrc2stats) {
671
         // process packet loss stats
675
         // process packet loss stats
679
         bitrateDownload += ssrcStats.bitrate.download;
683
         bitrateDownload += ssrcStats.bitrate.download;
680
         bitrateUpload += ssrcStats.bitrate.upload;
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
     this.eventEmitter.emit(
738
     this.eventEmitter.emit(
692
         StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
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
     this.conferenceStats.packetLoss = {
756
     this.conferenceStats.packetLoss = {
699
         total:
757
         total:
704
         upload:
762
         upload:
705
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
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
     this.conferenceStats.transport = [];
776
     this.conferenceStats.transport = [];
716
 };
777
 };
717
 
778
 
772
             // seems to vary between 0 and around 32k.
833
             // seems to vary between 0 and around 32k.
773
             audioLevel = audioLevel / 32767;
834
             audioLevel = audioLevel / 32767;
774
             this.eventEmitter.emit(
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 Ver arquivo

95
  * @param options
95
  * @param options
96
  */
96
  */
97
 export default function Statistics(xmpp, options) {
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
     this.eventEmitter = new EventEmitter();
104
     this.eventEmitter = new EventEmitter();
100
     this.xmpp = xmpp;
105
     this.xmpp = xmpp;
101
     this.options = options || {};
106
     this.options = options || {};
145
     }
150
     }
146
 });
151
 });
147
 
152
 
153
+/**
154
+ * Starts collecting RTP stats for given peerconnection.
155
+ * @param {TraceablePeerConnection} peerconnection
156
+ */
148
 Statistics.prototype.startRemoteStats = function(peerconnection) {
157
 Statistics.prototype.startRemoteStats = function(peerconnection) {
149
-    this.stopRemoteStats();
158
+    this.stopRemoteStats(peerconnection);
150
 
159
 
151
     try {
160
     try {
152
-        this.rtpStats
161
+        const rtpStats
153
             = new RTPStats(peerconnection,
162
             = new RTPStats(peerconnection,
154
                     Statistics.audioLevelsInterval, 2000, this.eventEmitter);
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
     } catch (e) {
167
     } catch (e) {
157
-        this.rtpStats = null;
158
         logger.error(`Failed to start collecting remote statistics: ${e}`);
168
         logger.error(`Failed to start collecting remote statistics: ${e}`);
159
     }
169
     }
160
 };
170
 };
229
         for (const callStats of this.callsStatsInstances.values()) {
239
         for (const callStats of this.callsStatsInstances.values()) {
230
             this.stopCallStats(callStats.tpc);
240
             this.stopCallStats(callStats.tpc);
231
         }
241
         }
232
-        this.stopRemoteStats();
242
+        for (const tpcId of this.rtpStatsMap.keys()) {
243
+            this._stopRemoteStats(tpcId);
244
+        }
233
         if (this.eventEmitter) {
245
         if (this.eventEmitter) {
234
             this.eventEmitter.removeAllListeners();
246
             this.eventEmitter.removeAllListeners();
235
         }
247
         }
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
 // CALSTATS METHODS
290
 // CALSTATS METHODS

Carregando…
Cancelar
Salvar