Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable31] Fix (known) false positives in connection warning #14252

Merged
merged 6 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
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
18 changes: 16 additions & 2 deletions src/utils/webrtc/analyzers/AverageStatValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const STAT_VALUE_TYPE = {
*
* The number of items to keep track of must be set when the AverageStatValue is
* created. Once N items have been added adding a new one will discard the
* oldest value. "hasEnoughData()" can be used to check if at least N items have
* oldest value. "hasEnoughData()" can be used to check if enough items have
* been added already and the average is reliable.
*
* An RTCStatsReport value can be cumulative since the creation of the
Expand All @@ -34,6 +34,10 @@ const STAT_VALUE_TYPE = {
* however, that the first value added to a cumulative AverageStatValue after
* creating or resetting it will be treated as 0 in the average calculation,
* as it will be the base from which the rest of relative values are calculated.
* Therefore, if the values added to an AverageStatValue are relative,
* "hasEnoughData()" will not return true until at least N items were added,
* but if the values are cumulative, it will not return true until at least N+1
* items were added.
*
* Besides the weighted average it is possible to "peek" the last value, either
* the raw value that was added or the relative one after the conversion (which,
Expand All @@ -54,15 +58,25 @@ function AverageStatValue(count, type = STAT_VALUE_TYPE.CUMULATIVE, lastValueWei

this._rawValues = []
this._relativeValues = []

this._hasEnoughData = false
}
AverageStatValue.prototype = {

reset() {
this._rawValues = []
this._relativeValues = []

this._hasEnoughData = false
},

add(value) {
if ((this._type === STAT_VALUE_TYPE.CUMULATIVE && this._rawValues.length === this._count)
|| (this._type === STAT_VALUE_TYPE.RELATIVE && this._rawValues.length >= (this._count - 1))
) {
this._hasEnoughData = true
}

if (this._rawValues.length === this._count) {
this._rawValues.shift()
this._relativeValues.shift()
Expand Down Expand Up @@ -97,7 +111,7 @@ AverageStatValue.prototype = {
},

hasEnoughData() {
return this._rawValues.length === this._count
return this._hasEnoughData
},

getWeightedAverage() {
Expand Down
93 changes: 84 additions & 9 deletions src/utils/webrtc/analyzers/AverageStatValue.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,92 @@ describe('AverageStatValue', () => {
})
})

test('returns whether there are enough values for a meaningful calculation', () => {
const testValues = [100, 200, 150, 123, 30, 50, 22, 33]
const stat = new AverageStatValue(3, STAT_VALUE_TYPE.CUMULATIVE, 3)
const stat2 = new AverageStatValue(3, STAT_VALUE_TYPE.RELATIVE, 3)
describe('returns whether there are enough values for a meaningful calculation', () => {
test('after creating', () => {
const testValues = [100, 200, 150, 123, 30, 50, 22, 33]
const stat = new AverageStatValue(3, STAT_VALUE_TYPE.CUMULATIVE, 3)
const stat2 = new AverageStatValue(3, STAT_VALUE_TYPE.RELATIVE, 3)

testValues.forEach((val, index) => {
stat.add(val)
expect(stat.hasEnoughData()).toBe(index >= 2)
testValues.forEach((val, index) => {
stat.add(val)
expect(stat.hasEnoughData()).toBe(index >= 3)

stat2.add(val)
expect(stat2.hasEnoughData()).toBe(index >= 2)
stat2.add(val)
expect(stat2.hasEnoughData()).toBe(index >= 2)
})
})

describe('resetting', () => {
let stat
let stat2

const addValues = (values) => {
values.forEach(val => {
stat.add(val)
stat2.add(val)
})
}

beforeEach(() => {
stat = new AverageStatValue(3, STAT_VALUE_TYPE.CUMULATIVE, 3)
stat2 = new AverageStatValue(3, STAT_VALUE_TYPE.RELATIVE, 3)
})

test('before having enough values', () => {
addValues([100, 200])

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(false)

stat.reset()
stat2.reset()

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(false)

addValues([150, 123])

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(false)

addValues([30])

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(true)

addValues([50])

expect(stat.hasEnoughData()).toBe(true)
expect(stat2.hasEnoughData()).toBe(true)
})

test('after having enough values', () => {
addValues([100, 200, 150, 123])

expect(stat.hasEnoughData()).toBe(true)
expect(stat2.hasEnoughData()).toBe(true)

stat.reset()
stat2.reset()

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(false)

addValues([30, 50])

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(false)

addValues([22])

expect(stat.hasEnoughData()).toBe(false)
expect(stat2.hasEnoughData()).toBe(true)

addValues([33])

expect(stat.hasEnoughData()).toBe(true)
expect(stat2.hasEnoughData()).toBe(true)
})
})
})

Expand Down
61 changes: 51 additions & 10 deletions src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ PeerConnectionAnalyzer.prototype = {
packetsLost[kind] = this._packetsLost[kind].getLastRawValue()
}

// In some (also strange) cases a newer stat may report a lower
// value than a previous one (it happens sometimes with garbage
// remote reports in simulcast video that cause the values to
// overflow, although it was also seen with a small value regression
// when enabling video). If that happens the stats are reset to
// prevent distorting the analysis with negative packet counts; note
// that in this case the previous value is not kept because it is
// not just an isolated wrong value, all the following stats
// increase from the regressed value.
if (packets[kind] >= 0 && packets[kind] < this._packets[kind].getLastRawValue()) {
this._resetStats(kind)
}

this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind], roundTripTime[kind])
}
},
Expand Down Expand Up @@ -466,12 +479,13 @@ PeerConnectionAnalyzer.prototype = {
* 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.
* as in the previous report (timestamp and round trip time may be updated
* or not, apparently depending on browser version and/or Janus version). 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
Expand Down Expand Up @@ -536,6 +550,18 @@ PeerConnectionAnalyzer.prototype = {
let packetsLostTotal = 0
let timestampsTotal = 0

// If the first timestamp stalled it is assumed that all of them
// stalled and are thus evenly distributed based on the new timestamp.
if (this._stagedTimestamps[kind][0] === timestampsBase) {
const lastTimestamp = this._stagedTimestamps[kind][this._stagedTimestamps[kind].length - 1]
const timestampsTotalDifference = lastTimestamp - timestampsBase
const timestampsDelta = timestampsTotalDifference / this._stagedTimestamps[kind].length

for (let i = 0; i < this._stagedTimestamps[kind].length - 1; i++) {
this._stagedTimestamps[kind][i] += timestampsDelta * (i + 1)
}
}

for (let i = 0; i < this._stagedPackets[kind].length; i++) {
packetsTotal += (this._stagedPackets[kind][i] - packetsBase)
packetsBase = this._stagedPackets[kind][i]
Expand All @@ -562,7 +588,11 @@ PeerConnectionAnalyzer.prototype = {
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.
// values may be properly updated even if the stats are stalled. In
// case they were not timestamps were already evenly distributed
// above, and round trip time can not be distributed, as it is
// already provided in the stats as a relative value rather than a
// cumulative one.
}
},

Expand Down Expand Up @@ -612,11 +642,19 @@ PeerConnectionAnalyzer.prototype = {
},

_calculateConnectionQuality(kind) {
const packets = this._packets[kind]
const packetsLost = this._packetsLost[kind]
const timestamps = this._timestamps[kind]
const packetsLostRatio = this._packetsLostRatio[kind]
const packetsPerSecond = this._packetsPerSecond[kind]
const roundTripTime = this._roundTripTime[kind]

if (!packetsLostRatio.hasEnoughData() || !packetsPerSecond.hasEnoughData()) {
// packetsLostRatio and packetsPerSecond are relative values, but they
// are calculated from cumulative values. Therefore, it is necessary to
// check if the cumulative values that are their source have enough data
// or not, rather than checking if the relative values themselves have
// enough data.
if (!packets.hasEnoughData() || !packetsLost.hasEnoughData() || !timestamps.hasEnoughData()) {
return CONNECTION_QUALITY.UNKNOWN
}

Expand Down Expand Up @@ -655,10 +693,13 @@ PeerConnectionAnalyzer.prototype = {
// quality to keep a smooth video, albeit on a lower resolution. Thus
// with a threshold of 10 packets issues can be detected too for videos,
// although only once they can not be further downscaled.
// Despite all of the above it has been observed that less than 10
// packets are sometimes sent without any connection problem (for
// example, when the background is blurred and the video quality is
// reduced due to being in a call with several participants), so for now
// it is only logged but not reported.
if (packetsPerSecond.getWeightedAverage() < 10) {
this._logStats(kind, 'Low packets per second: ' + packetsPerSecond.getWeightedAverage())

return CONNECTION_QUALITY.VERY_BAD
}

if (packetsLostRatioWeightedAverage > 0.3) {
Expand Down
Loading
Loading