From 587a116f3a1b5b65b14e5d2b11d82b700b939bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Jun 2021 00:53:01 +0200 Subject: [PATCH 1/7] Add helper method to get a string representation of an AverageStatValue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../webrtc/analyzers/AverageStatValue.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/utils/webrtc/analyzers/AverageStatValue.js b/src/utils/webrtc/analyzers/AverageStatValue.js index 9ba6c4dd6a9..544da04c901 100644 --- a/src/utils/webrtc/analyzers/AverageStatValue.js +++ b/src/utils/webrtc/analyzers/AverageStatValue.js @@ -55,6 +55,9 @@ const STAT_VALUE_TYPE = { * the raw value that was added or the relative one after the conversion (which, * for non cumulative values, will be the raw value too). * + * A string representation of the current relative values can be got by calling + * "toString()". + * * @param {int} count the number of instances to take into account. * @param {STAT_VALUE_TYPE} type whether the value is cumulative or relative. * @param {int} lastValueWeight the value to calculate the weights of all the @@ -127,6 +130,22 @@ AverageStatValue.prototype = { return weightedValues / weightsSum }, + toString() { + if (!this._relativeValues.length) { + return '[]' + } + + let relativeValuesAsString = '[' + this._relativeValues[0] + + for (let i = 1; i < this._relativeValues.length; i++) { + relativeValuesAsString += ', ' + this._relativeValues[i] + } + + relativeValuesAsString += ']' + + return relativeValuesAsString + }, + } export { From 185f919d9a4df0a57d471b17691e07b2ed1b87a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Jun 2021 00:53:33 +0200 Subject: [PATCH 2/7] Add logs for connection quality warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The logs are printed when the PeerConnectionAnalyzer changes to a state that causes a connection quality warning to be shown (very bad quality or not transmitted data). Currently the connection quality is analyzed only when the HPB is used and only for the sender participant, so there will be at most a single PeerConnectionAnalyzer. Due to this, for simplicity, for now the stats are logged without any participant identifier. Signed-off-by: Daniel Calviño Sánchez --- .../analyzers/PeerConnectionAnalyzer.js | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index d5074ed1fc1..bce808d8b1f 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -101,6 +101,10 @@ function PeerConnectionAnalyzer() { 'audio': new AverageStatValue(2, STAT_VALUE_TYPE.CUMULATIVE), 'video': new AverageStatValue(2, STAT_VALUE_TYPE.CUMULATIVE), } + this._timestampsForLogs = { + 'audio': new AverageStatValue(5, STAT_VALUE_TYPE.CUMULATIVE), + 'video': new AverageStatValue(5, STAT_VALUE_TYPE.CUMULATIVE), + } this._analysisEnabled = { 'audio': true, @@ -224,6 +228,7 @@ PeerConnectionAnalyzer.prototype = { this._packetsLostRatio[kind].reset() this._packetsPerSecond[kind].reset() this._timestamps[kind].reset() + this._timestampsForLogs[kind].reset() }, _handleIceConnectionStateChanged: function() { @@ -405,6 +410,7 @@ PeerConnectionAnalyzer.prototype = { } if (timestamp[kind] >= 0) { this._timestamps[kind].add(timestamp[kind]) + this._timestampsForLogs[kind].add(timestamp[kind]) } if (packets[kind] >= 0 && timestamp[kind] >= 0) { const elapsedSeconds = this._timestamps[kind].getLastRelativeValue() / 1000 @@ -495,6 +501,7 @@ PeerConnectionAnalyzer.prototype = { } if (timestamp[kind] >= 0) { this._timestamps[kind].add(timestamp[kind]) + this._timestampsForLogs[kind].add(timestamp[kind]) } if (packets[kind] >= 0 && timestamp[kind] >= 0) { const elapsedSeconds = this._timestamps[kind].getLastRelativeValue() / 1000 @@ -507,20 +514,22 @@ PeerConnectionAnalyzer.prototype = { }, _calculateConnectionQualityAudio: function() { - return this._calculateConnectionQuality(this._packetsLostRatio['audio'], this._packetsPerSecond['audio'], this._roundTripTime['audio']) + return this._calculateConnectionQuality(this._packetsLostRatio['audio'], this._packetsPerSecond['audio'], this._roundTripTime['audio'], 'audio') }, _calculateConnectionQualityVideo: function() { - return this._calculateConnectionQuality(this._packetsLostRatio['video'], this._packetsPerSecond['video'], this._roundTripTime['video']) + return this._calculateConnectionQuality(this._packetsLostRatio['video'], this._packetsPerSecond['video'], this._roundTripTime['video'], 'video') }, - _calculateConnectionQuality: function(packetsLostRatio, packetsPerSecond, roundTripTime) { + _calculateConnectionQuality: function(packetsLostRatio, packetsPerSecond, roundTripTime, kind) { if (!packetsLostRatio.hasEnoughData() || !packetsPerSecond.hasEnoughData()) { return CONNECTION_QUALITY.UNKNOWN } const packetsLostRatioWeightedAverage = packetsLostRatio.getWeightedAverage() if (packetsLostRatioWeightedAverage >= 1) { + this._logStats(kind, 'No transmitted data, packet lost ratio: ' + packetsLostRatioWeightedAverage) + return CONNECTION_QUALITY.NO_TRANSMITTED_DATA } @@ -529,6 +538,8 @@ PeerConnectionAnalyzer.prototype = { // discarded to try to keep the playing rate in real time. // Round trip time is measured in seconds. if (roundTripTime.hasEnoughData() && roundTripTime.getWeightedAverage() > 1.5) { + this._logStats(kind, 'High round trip time: ' + roundTripTime.getWeightedAverage()) + return CONNECTION_QUALITY.VERY_BAD } @@ -544,10 +555,14 @@ PeerConnectionAnalyzer.prototype = { // with a threshold of 10 packets issues can be detected too for videos, // although only once they can not be further downscaled. if (packetsPerSecond.getWeightedAverage() < 10) { + this._logStats(kind, 'Low packets per second: ' + packetsPerSecond.getWeightedAverage()) + return CONNECTION_QUALITY.VERY_BAD } if (packetsLostRatioWeightedAverage > 0.3) { + this._logStats(kind, 'High packet lost ratio: ' + packetsLostRatioWeightedAverage) + return CONNECTION_QUALITY.VERY_BAD } @@ -562,6 +577,21 @@ PeerConnectionAnalyzer.prototype = { return CONNECTION_QUALITY.GOOD }, + _logStats: function(kind, message) { + const tag = 'PeerConnectionAnalyzer: ' + kind + ': ' + + if (message) { + console.debug(tag + message) + } + + console.debug(tag + 'Packets: ' + this._packets[kind].toString()) + console.debug(tag + 'Packets lost: ' + this._packetsLost[kind].toString()) + console.debug(tag + 'Packets lost ratio: ' + this._packetsLostRatio[kind].toString()) + console.debug(tag + 'Packets per second: ' + this._packetsPerSecond[kind].toString()) + console.debug(tag + 'Round trip time: ' + this._roundTripTime[kind].toString()) + console.debug(tag + 'Timestamps: ' + this._timestampsForLogs[kind].toString()) + }, + } export { From f8f40a615fa4a3bdd0386bab6f2d94529e75852f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 24 Jun 2021 12:28:37 +0200 Subject: [PATCH 3/7] Fix stats being reset when enabling the analysis twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stats were reset whenever "setAnalysisEnabledXXX(true)" was called. Due to this, if the analysis for audio or video was already enabled and the method was called again the stats were wrongly reset. The stats should be reset only when the analysis is really started again. Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index bce808d8b1f..cb7f69f5726 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -203,6 +203,10 @@ PeerConnectionAnalyzer.prototype = { }, setAnalysisEnabledAudio: function(analysisEnabledAudio) { + if (this._analysisEnabled['audio'] === analysisEnabledAudio) { + return + } + this._analysisEnabled['audio'] = analysisEnabledAudio if (!analysisEnabledAudio) { @@ -213,6 +217,10 @@ PeerConnectionAnalyzer.prototype = { }, setAnalysisEnabledVideo: function(analysisEnabledVideo) { + if (this._analysisEnabled['video'] === analysisEnabledVideo) { + return + } + this._analysisEnabled['video'] = analysisEnabledVideo if (!analysisEnabledVideo) { From 5bb110bfbc818025635a627ca72fcb558d3070cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Jun 2021 12:39:46 +0200 Subject: [PATCH 4/7] Convert "_connectionQuality" attribute to an array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the attribute consistent with the stats, and will also make possible to directly get the quality value given a string identifying its kind ("audio" or "video"). Signed-off-by: Daniel Calviño Sánchez --- .../webrtc/analyzers/PeerConnectionAnalyzer.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index cb7f69f5726..b3b11c1d873 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -121,8 +121,10 @@ function PeerConnectionAnalyzer() { this._handleIceConnectionStateChangedBound = this._handleIceConnectionStateChanged.bind(this) this._processStatsBound = this._processStats.bind(this) - this._connectionQualityAudio = CONNECTION_QUALITY.UNKNOWN - this._connectionQualityVideo = CONNECTION_QUALITY.UNKNOWN + this._connectionQuality = { + 'audio': CONNECTION_QUALITY.UNKNOWN, + 'video': CONNECTION_QUALITY.UNKNOWN, + } } PeerConnectionAnalyzer.prototype = { @@ -162,28 +164,28 @@ PeerConnectionAnalyzer.prototype = { }, getConnectionQualityAudio: function() { - return this._connectionQualityAudio + return this._connectionQuality['audio'] }, getConnectionQualityVideo: function() { - return this._connectionQualityVideo + return this._connectionQuality['video'] }, _setConnectionQualityAudio: function(connectionQualityAudio) { - if (this._connectionQualityAudio === connectionQualityAudio) { + if (this._connectionQuality['audio'] === connectionQualityAudio) { return } - this._connectionQualityAudio = connectionQualityAudio + this._connectionQuality['audio'] = connectionQualityAudio this._trigger('change:connectionQualityAudio', [connectionQualityAudio]) }, _setConnectionQualityVideo: function(connectionQualityVideo) { - if (this._connectionQualityVideo === connectionQualityVideo) { + if (this._connectionQuality['video'] === connectionQualityVideo) { return } - this._connectionQualityVideo = connectionQualityVideo + this._connectionQuality['video'] = connectionQualityVideo this._trigger('change:connectionQualityVideo', [connectionQualityVideo]) }, From 1352f12ceb6bdf74994eaffcb87d07b631574762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Jun 2021 12:44:49 +0200 Subject: [PATCH 5/7] Get attributes based on kind instead of explictly pass them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index b3b11c1d873..7ecf62989ce 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -524,14 +524,18 @@ PeerConnectionAnalyzer.prototype = { }, _calculateConnectionQualityAudio: function() { - return this._calculateConnectionQuality(this._packetsLostRatio['audio'], this._packetsPerSecond['audio'], this._roundTripTime['audio'], 'audio') + return this._calculateConnectionQuality('audio') }, _calculateConnectionQualityVideo: function() { - return this._calculateConnectionQuality(this._packetsLostRatio['video'], this._packetsPerSecond['video'], this._roundTripTime['video'], 'video') + return this._calculateConnectionQuality('video') }, - _calculateConnectionQuality: function(packetsLostRatio, packetsPerSecond, roundTripTime, kind) { + _calculateConnectionQuality: function(kind) { + const packetsLostRatio = this._packetsLostRatio[kind] + const packetsPerSecond = this._packetsPerSecond[kind] + const roundTripTime = this._roundTripTime[kind] + if (!packetsLostRatio.hasEnoughData() || !packetsPerSecond.hasEnoughData()) { return CONNECTION_QUALITY.UNKNOWN } From 74bb926aa50d45fd0aa3e0b418752de1185b2e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Jun 2021 12:47:20 +0200 Subject: [PATCH 6/7] Extract adding the processed stats to its own method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../analyzers/PeerConnectionAnalyzer.js | 98 +++++++------------ 1 file changed, 36 insertions(+), 62 deletions(-) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index 7ecf62989ce..c4e5a1c8eba 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -399,39 +399,7 @@ PeerConnectionAnalyzer.prototype = { packetsLost[kind] = this._packetsLost[kind].getLastRawValue() } - if (packets[kind] >= 0) { - this._packets[kind].add(packets[kind]) - } - if (packetsLost[kind] >= 0) { - this._packetsLost[kind].add(packetsLost[kind]) - } - if (packets[kind] >= 0 && packetsLost[kind] >= 0) { - // The packet stats are cumulative values, so the isolated - // values are got from the helper object. - // If there were no transmitted packets in the last stats the - // ratio is higher than 1 both to signal that and to force the - // quality towards a very bad quality faster, but not - // immediately. - let packetsLostRatio = 1.5 - if (this._packets[kind].getLastRelativeValue() > 0) { - packetsLostRatio = this._packetsLost[kind].getLastRelativeValue() / this._packets[kind].getLastRelativeValue() - } - this._packetsLostRatio[kind].add(packetsLostRatio) - } - if (timestamp[kind] >= 0) { - this._timestamps[kind].add(timestamp[kind]) - this._timestampsForLogs[kind].add(timestamp[kind]) - } - if (packets[kind] >= 0 && timestamp[kind] >= 0) { - const elapsedSeconds = this._timestamps[kind].getLastRelativeValue() / 1000 - // The packet stats are cumulative values, so the isolated - // values are got from the helper object. - const packetsPerSecond = this._packets[kind].getLastRelativeValue() / elapsedSeconds - this._packetsPerSecond[kind].add(packetsPerSecond) - } - if (roundTripTime[kind] >= 0) { - this._roundTripTime[kind].add(roundTripTime[kind]) - } + this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind], roundTripTime[kind]) } }, @@ -490,36 +458,42 @@ PeerConnectionAnalyzer.prototype = { packetsLost[kind] = this._packetsLost[kind].getLastRawValue() } - if (packets[kind] >= 0) { - this._packets[kind].add(packets[kind]) - } - if (packetsLost[kind] >= 0) { - this._packetsLost[kind].add(packetsLost[kind]) - } - if (packets[kind] >= 0 && packetsLost[kind] >= 0) { - // The packet stats are cumulative values, so the isolated - // values are got from the helper object. - // If there were no transmitted packets in the last stats the - // ratio is higher than 1 both to signal that and to force the - // quality towards a very bad quality faster, but not - // immediately. - let packetsLostRatio = 1.5 - if (this._packets[kind].getLastRelativeValue() > 0) { - packetsLostRatio = this._packetsLost[kind].getLastRelativeValue() / this._packets[kind].getLastRelativeValue() - } - this._packetsLostRatio[kind].add(packetsLostRatio) - } - if (timestamp[kind] >= 0) { - this._timestamps[kind].add(timestamp[kind]) - this._timestampsForLogs[kind].add(timestamp[kind]) - } - if (packets[kind] >= 0 && timestamp[kind] >= 0) { - const elapsedSeconds = this._timestamps[kind].getLastRelativeValue() / 1000 - // The packet stats are cumulative values, so the isolated - // values are got from the helper object. - const packetsPerSecond = this._packets[kind].getLastRelativeValue() / elapsedSeconds - this._packetsPerSecond[kind].add(packetsPerSecond) + this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind]) + } + }, + + _addStats: function(kind, packets, packetsLost, timestamp, roundTripTime) { + if (packets >= 0) { + this._packets[kind].add(packets) + } + if (packetsLost >= 0) { + this._packetsLost[kind].add(packetsLost) + } + if (packets >= 0 && packetsLost >= 0) { + // The packet stats are cumulative values, so the isolated values + // are got from the helper object. + // If there were no transmitted packets in the last stats the ratio + // is higher than 1 both to signal that and to force the quality + // towards a very bad quality faster, but not immediately. + let packetsLostRatio = 1.5 + if (this._packets[kind].getLastRelativeValue() > 0) { + packetsLostRatio = this._packetsLost[kind].getLastRelativeValue() / this._packets[kind].getLastRelativeValue() } + this._packetsLostRatio[kind].add(packetsLostRatio) + } + if (timestamp >= 0) { + this._timestamps[kind].add(timestamp) + this._timestampsForLogs[kind].add(timestamp) + } + if (packets >= 0 && timestamp >= 0) { + const elapsedSeconds = this._timestamps[kind].getLastRelativeValue() / 1000 + // The packet stats are cumulative values, so the isolated + // values are got from the helper object. + const packetsPerSecond = this._packets[kind].getLastRelativeValue() / elapsedSeconds + this._packetsPerSecond[kind].add(packetsPerSecond) + } + if (roundTripTime !== undefined && roundTripTime >= 0) { + this._roundTripTime[kind].add(roundTripTime) } }, From 416bae0c1c8385dc463043e881dd78e1d493acd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 28 Jun 2021 05:31:12 +0200 Subject: [PATCH 7/7] Fix very bad quality reported due to stalled stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no packets are transmitted the packet lost ratio is set to a value higher than 1 to move towards a "no transmitted data" state faster, although not immediately. Both the "no transmitted data" and "very bad quality" states are based on the packet lost ratio, so this causes the "very bad quality" state to be triggered as soon as no packets are transmitted. However, the stats reported by the browser can sometimes stall for a second (it is unclear whether the browser does not update the stats or Janus does not send them, though). When that happens the stats are still reported, but with the same values as in the previous report. As there were no transmitted packets the "very bad quality" state was (wrongly) triggered. To solve this now if there were no transmitted packets in the last report the analysis is kept on hold. If in the next report the number of packets has changed then the previous report is considered to have stalled and the new values are distributed between the previous and current report. If the number of packets is still zero then the previous report is considered to have been legit and the previous and current values are added as normal. Signed-off-by: Daniel Calviño Sánchez --- .../analyzers/PeerConnectionAnalyzer.js | 135 +++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index c4e5a1c8eba..6da1984a2d4 100644 --- a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js +++ b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js @@ -106,6 +106,23 @@ function PeerConnectionAnalyzer() { 'video': new AverageStatValue(5, STAT_VALUE_TYPE.CUMULATIVE), } + this._stagedPackets = { + 'audio': [], + 'video': [], + } + this._stagedPacketsLost = { + 'audio': [], + 'video': [], + } + this._stagedRoundTripTime = { + 'audio': [], + 'video': [], + } + this._stagedTimestamps = { + 'audio': [], + 'video': [], + } + this._analysisEnabled = { 'audio': true, 'video': true, @@ -462,7 +479,114 @@ PeerConnectionAnalyzer.prototype = { } }, + /** + * Adds the stats reported by the browser to the average stats used to do + * the analysis. + * + * The stats reported by the browser can sometimes stall for a second (or + * more, but typically they stall only for a single report). When that + * happens the stats are still reported, but with the same number of packets + * as in the previous report (timestamp and round trip time are updated, + * though). In that case the given stats are not added yet to the average + * stats; they are kept on hold until more stats are provided by the browser + * and it can be determined if the previous stats were stalled or not. If + * they were stalled the previous and new stats are distributed, and if they + * were not they are added as is to the average stats. + * + * @param {string} kind the type of the stats ("audio" or "video") + * @param {number} packets the cumulative number of packets + * @param {number} packetsLost the cumulative number of lost packets + * @param {number} timestamp the cumulative timestamp + * @param {number} roundTripTime the relative round trip time + */ _addStats: function(kind, packets, packetsLost, timestamp, roundTripTime) { + if (this._stagedPackets[kind].length === 0) { + if (packets !== this._packets[kind].getLastRawValue()) { + this._commitStats(kind, packets, packetsLost, timestamp, roundTripTime) + } else { + this._stageStats(kind, packets, packetsLost, timestamp, roundTripTime) + } + + return + } + + this._stageStats(kind, packets, packetsLost, timestamp, roundTripTime) + + // If the packets have changed now it is assumed that the previous stats + // were stalled. + if (packets > 0) { + this._distributeStagedStats(kind) + } + + while (this._stagedPackets[kind].length > 0) { + const stagedPackets = this._stagedPackets[kind].shift() + const stagedPacketsLost = this._stagedPacketsLost[kind].shift() + const stagedTimestamp = this._stagedTimestamps[kind].shift() + const stagedRoundTripTime = this._stagedRoundTripTime[kind].shift() + + this._commitStats(kind, stagedPackets, stagedPacketsLost, stagedTimestamp, stagedRoundTripTime) + } + }, + + _stageStats: function(kind, packets, packetsLost, timestamp, roundTripTime) { + this._stagedPackets[kind].push(packets) + this._stagedPacketsLost[kind].push(packetsLost) + this._stagedTimestamps[kind].push(timestamp) + this._stagedRoundTripTime[kind].push(roundTripTime) + }, + + /** + * Distributes the values of the staged stats proportionately to their + * timestamps. + * + * Once the stats unstall the new stats are a sum of the values that should + * have been reported before and the actual new values. The stats typically + * stall for just a second, but they can stall for an arbitrary length too. + * Due to this the staged stats need to be distributed based on their + * timestamps. + * + * @param {string} kind the type of the stats ("audio" or "video") + */ + _distributeStagedStats: function(kind) { + let packetsBase = this._packets[kind].getLastRawValue() + let packetsLostBase = this._packetsLost[kind].getLastRawValue() + let timestampsBase = this._timestamps[kind].getLastRawValue() + + let packetsTotal = 0 + let packetsLostTotal = 0 + let timestampsTotal = 0 + + for (let i = 0; i < this._stagedPackets[kind].length; i++) { + packetsTotal += (this._stagedPackets[kind][i] - packetsBase) + packetsBase = this._stagedPackets[kind][i] + + packetsLostTotal += (this._stagedPacketsLost[kind][i] - packetsLostBase) + packetsLostBase = this._stagedPacketsLost[kind][i] + + timestampsTotal += (this._stagedTimestamps[kind][i] - timestampsBase) + timestampsBase = this._stagedTimestamps[kind][i] + } + + packetsBase = this._packets[kind].getLastRawValue() + packetsLostBase = this._packetsLost[kind].getLastRawValue() + timestampsBase = this._timestamps[kind].getLastRawValue() + + for (let i = 0; i < this._stagedPackets[kind].length; i++) { + const weight = (this._stagedTimestamps[kind][i] - timestampsBase) / timestampsTotal + timestampsBase = this._stagedTimestamps[kind][i] + + this._stagedPackets[kind][i] = packetsBase + packetsTotal * weight + packetsBase = this._stagedPackets[kind][i] + + this._stagedPacketsLost[kind][i] = packetsLostBase + packetsLostTotal * weight + packetsLostBase = this._stagedPacketsLost[kind][i] + + // Timestamps and round trip time are not distributed, as those + // values are properly updated even if the stats are stalled. + } + }, + + _commitStats: function(kind, packets, packetsLost, timestamp, roundTripTime) { if (packets >= 0) { this._packets[kind].add(packets) } @@ -474,7 +598,9 @@ PeerConnectionAnalyzer.prototype = { // are got from the helper object. // If there were no transmitted packets in the last stats the ratio // is higher than 1 both to signal that and to force the quality - // towards a very bad quality faster, but not immediately. + // towards "no transmitted data" faster, but not immediately. + // However, note that the quality will immediately change to "very + // bad quality". let packetsLostRatio = 1.5 if (this._packets[kind].getLastRelativeValue() > 0) { packetsLostRatio = this._packetsLost[kind].getLastRelativeValue() / this._packets[kind].getLastRelativeValue() @@ -514,6 +640,13 @@ PeerConnectionAnalyzer.prototype = { return CONNECTION_QUALITY.UNKNOWN } + // The stats might be in a temporary stall and the analysis is on hold + // until further stats arrive, so until that happens the last known + // state is returned again. + if (this._stagedPackets[kind].length > 0) { + return this._connectionQuality[kind] + } + const packetsLostRatioWeightedAverage = packetsLostRatio.getWeightedAverage() if (packetsLostRatioWeightedAverage >= 1) { this._logStats(kind, 'No transmitted data, packet lost ratio: ' + packetsLostRatioWeightedAverage)