Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 193 additions & 72 deletions src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {

Expand Down Expand Up @@ -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])
},

Expand All @@ -203,6 +222,10 @@ PeerConnectionAnalyzer.prototype = {
},

setAnalysisEnabledAudio(analysisEnabledAudio) {
if (this._analysisEnabled.audio === analysisEnabledAudio) {
return
}

this._analysisEnabled.audio = analysisEnabledAudio

if (!analysisEnabledAudio) {
Expand All @@ -213,6 +236,10 @@ PeerConnectionAnalyzer.prototype = {
},

setAnalysisEnabledVideo(analysisEnabledVideo) {
if (this._analysisEnabled.video === analysisEnabledVideo) {
return
}

this._analysisEnabled.video = analysisEnabledVideo

if (!analysisEnabledVideo) {
Expand Down Expand Up @@ -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])
}
},

Expand Down Expand Up @@ -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)
Expand Down