diff --git a/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js b/src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js index d1213db292..a9d0107133 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(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(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) } @@ -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)