Kaynağa Gözat

feat(stats): Add audio output problem detector.

master
Hristo Terezov 6 yıl önce
ebeveyn
işleme
070c67bf5d

+ 12
- 0
JitsiConference.js Dosyayı Görüntüle

@@ -28,6 +28,7 @@ import Jvb121EventGenerator from './modules/event/Jvb121EventGenerator';
28 28
 import RecordingManager from './modules/recording/RecordingManager';
29 29
 import RttMonitor from './modules/rttmonitor/rttmonitor';
30 30
 import AvgRTPStatsReporter from './modules/statistics/AvgRTPStatsReporter';
31
+import AudioOutputProblemDetector from './modules/statistics/AudioOutputProblemDetector';
31 32
 import SpeakerStatsCollector from './modules/statistics/SpeakerStatsCollector';
32 33
 import Statistics from './modules/statistics/statistics';
33 34
 import Transcriber from './modules/transcription/transcriber';
@@ -163,6 +164,12 @@ export default function JitsiConference(options) {
163 164
     this.avgRtpStatsReporter
164 165
         = new AvgRTPStatsReporter(this, options.config.avgRtpStatsN || 15);
165 166
 
167
+    /**
168
+     * Detects issues with the audio of remote participants.
169
+     * @type {AudioOutputProblemDetector}
170
+     */
171
+    this._audioOutputProblemDetector = new AudioOutputProblemDetector(this);
172
+
166 173
     /**
167 174
      * Indicates whether the connection is interrupted or not.
168 175
      */
@@ -455,6 +462,11 @@ JitsiConference.prototype.leave = function() {
455 462
         this.avgRtpStatsReporter = null;
456 463
     }
457 464
 
465
+    if (this._audioOutputProblemDetector) {
466
+        this._audioOutputProblemDetector.dispose();
467
+        this._audioOutputProblemDetector = null;
468
+    }
469
+
458 470
     if (this.rttMonitor) {
459 471
         this.rttMonitor.stop();
460 472
         this.rttMonitor = null;

+ 4
- 2
modules/connectivity/ConnectionQuality.js Dosyayı Görüntüle

@@ -453,7 +453,8 @@ export default class ConnectionQuality {
453 453
             packetLoss: this._localStats.packetLoss,
454 454
             connectionQuality: this._localStats.connectionQuality,
455 455
             jvbRTT: this._localStats.jvbRTT,
456
-            serverRegion: this._localStats.serverRegion
456
+            serverRegion: this._localStats.serverRegion,
457
+            avgAudioLevels: this._localStats.localAvgAudioLevels
457 458
         };
458 459
 
459 460
         try {
@@ -543,7 +544,8 @@ export default class ConnectionQuality {
543 544
             packetLoss: data.packetLoss,
544 545
             connectionQuality: data.connectionQuality,
545 546
             jvbRTT: data.jvbRTT,
546
-            serverRegion: data.serverRegion
547
+            serverRegion: data.serverRegion,
548
+            avgAudioLevels: data.avgAudioLevels
547 549
         };
548 550
 
549 551
         this.eventEmitter.emit(

+ 133
- 0
modules/statistics/AudioOutputProblemDetector.js Dosyayı Görüntüle

@@ -0,0 +1,133 @@
1
+import { getLogger } from 'jitsi-meet-logger';
2
+
3
+import * as ConferenceEvents from '../../JitsiConferenceEvents';
4
+import * as ConnectionQualityEvents from '../../service/connectivity/ConnectionQualityEvents';
5
+import * as MediaType from '../../service/RTC/MediaType';
6
+import { createAudioOutputProblemEvent } from '../../service/statistics/AnalyticsEvents';
7
+
8
+import Statistics from './statistics';
9
+
10
+const logger = getLogger(__filename);
11
+
12
+/**
13
+ * Number of remote samples that will be used for comparison with local ones.
14
+ */
15
+const NUMBER_OF_REMOTE_SAMPLES = 3;
16
+
17
+/**
18
+ * Collects the average audio levels per participant from the local stats and the stats received by every remote
19
+ * participant and compares them to detect potential audio problem for a participant.
20
+ */
21
+export default class AudioOutputProblemDetector {
22
+
23
+    /**
24
+     * Creates new <tt>AudioOutputProblemDetector</tt> instance.
25
+     *
26
+     * @param {JitsiCofnerence} conference - The conference instance to be monitored.
27
+     */
28
+    constructor(conference) {
29
+        this._conference = conference;
30
+        this._lastReceivedAudioLevel = {};
31
+        this._reportedParticipants = [];
32
+        this._onLocalAudioLevelsReport = this._onLocalAudioLevelsReport.bind(this);
33
+        this._onRemoteAudioLevelReceived = this._onRemoteAudioLevelReceived.bind(this);
34
+        this._clearUserData = this._clearUserData.bind(this);
35
+        this._conference.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
36
+        this._conference.statistics.addConnectionStatsListener(this._onLocalAudioLevelsReport);
37
+        this._conference.on(ConferenceEvents.USER_LEFT, this._clearUserData);
38
+    }
39
+
40
+    /**
41
+     * A listener for audio level data received by a remote participant.
42
+     *
43
+     * @param {string} userID - The user id of the participant that sent the data.
44
+     * @param {number} audioLevel - The average audio level value.
45
+     * @returns {void}
46
+     */
47
+    _onRemoteAudioLevelReceived(userID, { avgAudioLevels }) {
48
+        if (this._reportedParticipants.indexOf(userID) !== -1) {
49
+            return;
50
+        }
51
+
52
+        if (!Array.isArray(this._lastReceivedAudioLevel[userID])) {
53
+            this._lastReceivedAudioLevel[userID] = [ ];
54
+        } else if (this._lastReceivedAudioLevel[userID].length >= NUMBER_OF_REMOTE_SAMPLES) {
55
+            this._lastReceivedAudioLevel[userID].shift();
56
+        }
57
+
58
+        this._lastReceivedAudioLevel[userID].push(avgAudioLevels);
59
+
60
+    }
61
+
62
+    /**
63
+     * A listener for audio level data retrieved by the local stats.
64
+     *
65
+     * @param {TraceablePeerConnection} tpc - The <tt>TraceablePeerConnection</tt> instance used to gather the data.
66
+     * @param {Object} avgAudioLevels - The average audio levels per participant.
67
+     * @returns {void}
68
+     */
69
+    _onLocalAudioLevelsReport(tpc, { avgAudioLevels }) {
70
+        if (tpc !== this._conference.getActivePeerConnection()) {
71
+            return;
72
+        }
73
+
74
+        Object.keys(this._lastReceivedAudioLevel).forEach(userID => {
75
+            if (this._reportedParticipants.indexOf(userID) !== -1) {
76
+                // Do not report the participant again.
77
+                return;
78
+            }
79
+
80
+            const remoteAudioLevels = this._lastReceivedAudioLevel[userID];
81
+            const participant = this._conference.getParticipantById(userID);
82
+
83
+            if (participant) {
84
+                const tracks = participant.getTracksByMediaType(MediaType.AUDIO);
85
+
86
+                if (tracks.length > 0 && participant.isAudioMuted()) {
87
+                    // We don't need to report an error if everything seems fine with the participant and its tracks but
88
+                    // the participant is audio muted.
89
+                    return;
90
+                }
91
+            }
92
+
93
+            if ((!(userID in avgAudioLevels) || avgAudioLevels[userID] === 0)
94
+                    && Array.isArray(remoteAudioLevels)
95
+                    && remoteAudioLevels.length === NUMBER_OF_REMOTE_SAMPLES
96
+                    && remoteAudioLevels.every(audioLevel => audioLevel > 0)) {
97
+                const remoteAudioLevelsString = JSON.stringify(remoteAudioLevels);
98
+
99
+                Statistics.sendAnalytics(
100
+                    createAudioOutputProblemEvent(userID, avgAudioLevels[userID], remoteAudioLevelsString));
101
+                logger.warn(`A potential problem is detected with the audio output for participant ${
102
+                    userID}, local audio levels: ${avgAudioLevels[userID]}, remote audio levels: ${
103
+                    remoteAudioLevelsString}`);
104
+                this._reportedParticipants.push(userID);
105
+                this._clearUserData();
106
+            }
107
+        });
108
+    }
109
+
110
+    /**
111
+     * Clears the data stored for a participant.
112
+     *
113
+     * @param {string} userID - The id of the participant.
114
+     * @returns {void}
115
+     */
116
+    _clearUserData(userID) {
117
+        delete this._lastReceivedAudioLevel[userID];
118
+    }
119
+
120
+    /**
121
+     * Disposes the allocated resources.
122
+     *
123
+     * @returns {void}
124
+     */
125
+    dispose() {
126
+        this._conference.off(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
127
+        this._conference.off(ConferenceEvents.USER_LEFT, this._clearUserData);
128
+        this._conference.statistics.removeConnectionStatsListener(this._onLocalAudioLevelsReport);
129
+        this._lastReceivedAudioLevel = undefined;
130
+        this._reportedParticipants = undefined;
131
+        this._conference = undefined;
132
+    }
133
+}

+ 35
- 1
modules/statistics/RTPStatsCollector.js Dosyayı Görüntüle

@@ -257,6 +257,7 @@ export default function StatsCollector(
257 257
     this.currentAudioLevelsReport = null;
258 258
     this.currentStatsReport = null;
259 259
     this.previousStatsReport = null;
260
+    this.audioLevelReportHistory = {};
260 261
     this.audioLevelsIntervalId = null;
261 262
     this.eventEmitter = eventEmitter;
262 263
     this.conferenceStats = new ConferenceStats();
@@ -846,6 +847,29 @@ StatsCollector.prototype._processAndEmitReport = function() {
846 847
             calculatePacketLoss(lostPackets.upload, totalPackets.upload)
847 848
     };
848 849
 
850
+    const avgAudioLevels = {};
851
+    let localAvgAudioLevels;
852
+
853
+    Object.keys(this.audioLevelReportHistory).forEach(ssrc => {
854
+        const { data, isLocal } = this.audioLevelReportHistory[ssrc];
855
+        const avgAudioLevel = data.reduce((sum, currentValue) => sum + currentValue) / data.length;
856
+
857
+        if (isLocal) {
858
+            localAvgAudioLevels = avgAudioLevel;
859
+        } else {
860
+            const track = this.peerconnection.getTrackBySSRC(Number(ssrc));
861
+
862
+            if (track) {
863
+                const participantId = track.getParticipantId();
864
+
865
+                if (participantId) {
866
+                    avgAudioLevels[participantId] = avgAudioLevel;
867
+                }
868
+            }
869
+        }
870
+    });
871
+    this.audioLevelReportHistory = {};
872
+
849 873
     this.eventEmitter.emit(
850 874
         StatisticsEvents.CONNECTION_STATS,
851 875
         this.peerconnection,
@@ -855,7 +879,9 @@ StatsCollector.prototype._processAndEmitReport = function() {
855 879
             'packetLoss': this.conferenceStats.packetLoss,
856 880
             'resolution': resolutions,
857 881
             'framerate': framerates,
858
-            'transport': this.conferenceStats.transport
882
+            'transport': this.conferenceStats.transport,
883
+            localAvgAudioLevels,
884
+            avgAudioLevels
859 885
         });
860 886
     this.conferenceStats.transport = [];
861 887
 };
@@ -942,6 +968,14 @@ StatsCollector.prototype.processAudioLevelReport = function() {
942 968
                 audioLevel = audioLevel / 32767;
943 969
             }
944 970
 
971
+            if (!(ssrc in this.audioLevelReportHistory)) {
972
+                this.audioLevelReportHistory[ssrc] = {
973
+                    isLocal,
974
+                    data: []
975
+                };
976
+            }
977
+            this.audioLevelReportHistory[ssrc].data.push(audioLevel);
978
+
945 979
             this.eventEmitter.emit(
946 980
                 StatisticsEvents.AUDIO_LEVEL,
947 981
                 this.peerconnection,

+ 20
- 0
service/statistics/AnalyticsEvents.js Dosyayı Görüntüle

@@ -470,6 +470,26 @@ export const createRttByRegionEvent = function(attributes) {
470 470
     };
471 471
 };
472 472
 
473
+/**
474
+ * Creates an event which contains information about the audio output problem (the user id of the affected participant,
475
+ * the local audio levels and the remote audio levels that triggered the event).
476
+ *
477
+ * @param {string} userID - The user id of the affected participant.
478
+ * @param {*} localAudioLevel - The local audio levels.
479
+ * @param {*} remoteAudioLevels - The audio levels received from the participant.
480
+ */
481
+export function createAudioOutputProblemEvent(userID, localAudioLevel, remoteAudioLevels) {
482
+    return {
483
+        type: TYPE_OPERATIONAL,
484
+        action: 'audio.output.problem',
485
+        attributes: {
486
+            userID,
487
+            localAudioLevel,
488
+            remoteAudioLevels
489
+        }
490
+    };
491
+}
492
+
473 493
 /**
474 494
  * Creates an event which contains an information related to the bridge channel close event.
475 495
  *

Loading…
İptal
Kaydet