diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index 7b8b85cbbc9..a9d01071338 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, @@ -121,8 +138,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 +181,28 @@ PeerConnectionAnalyzer.prototype = { }, getConnectionQualityAudio() { - return this._connectionQualityAudio + return this._connectionQuality.audio }, getConnectionQualityVideo() { - return this._connectionQualityVideo + return this._connectionQuality.video }, _setConnectionQualityAudio(connectionQualityAudio) { - if (this._connectionQualityAudio === connectionQualityAudio) { + if (this._connectionQuality.audio === connectionQualityAudio) { return } - this._connectionQualityAudio = connectionQualityAudio + this._connectionQuality.audio = connectionQualityAudio this._trigger('change:connectionQualityAudio', [connectionQualityAudio]) }, _setConnectionQualityVideo(connectionQualityVideo) { - if (this._connectionQualityVideo === connectionQualityVideo) { + if (this._connectionQuality.video === connectionQualityVideo) { return } - this._connectionQualityVideo = connectionQualityVideo + this._connectionQuality.video = connectionQualityVideo this._trigger('change:connectionQualityVideo', [connectionQualityVideo]) }, @@ -203,6 +222,10 @@ PeerConnectionAnalyzer.prototype = { }, setAnalysisEnabledAudio(analysisEnabledAudio) { + if (this._analysisEnabled.audio === analysisEnabledAudio) { + return + } + this._analysisEnabled.audio = analysisEnabledAudio if (!analysisEnabledAudio) { @@ -213,6 +236,10 @@ PeerConnectionAnalyzer.prototype = { }, setAnalysisEnabledVideo(analysisEnabledVideo) { + if (this._analysisEnabled.video === analysisEnabledVideo) { + return + } + this._analysisEnabled.video = analysisEnabledVideo if (!analysisEnabledVideo) { @@ -389,39 +416,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]) } }, @@ -480,52 +475,178 @@ 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]) + this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind]) + } + }, + + /** + * 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(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) } - 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) + + 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(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(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(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 "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() } + 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) } }, _calculateConnectionQualityAudio() { - return this._calculateConnectionQuality(this._packetsLostRatio.audio, this._packetsPerSecond.audio, this._roundTripTime.audio, 'audio') + return this._calculateConnectionQuality('audio') }, _calculateConnectionQualityVideo() { - return this._calculateConnectionQuality(this._packetsLostRatio.video, this._packetsPerSecond.video, this._roundTripTime.video, 'video') + return this._calculateConnectionQuality('video') }, - _calculateConnectionQuality(packetsLostRatio, packetsPerSecond, roundTripTime, kind) { + _calculateConnectionQuality(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 } + // 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)