Skip to content

Commit ae4e70e

Browse files
authored
Merge pull request #14251 from nextcloud/backport/14095/stable30
[stable30] Fix (known) false positives in connection warning
2 parents a680e22 + 6c1aacc commit ae4e70e

File tree

4 files changed

+3515
-21
lines changed

4 files changed

+3515
-21
lines changed

src/utils/webrtc/analyzers/AverageStatValue.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const STAT_VALUE_TYPE = {
2020
*
2121
* The number of items to keep track of must be set when the AverageStatValue is
2222
* created. Once N items have been added adding a new one will discard the
23-
* oldest value. "hasEnoughData()" can be used to check if at least N items have
23+
* oldest value. "hasEnoughData()" can be used to check if enough items have
2424
* been added already and the average is reliable.
2525
*
2626
* An RTCStatsReport value can be cumulative since the creation of the
@@ -34,6 +34,10 @@ const STAT_VALUE_TYPE = {
3434
* however, that the first value added to a cumulative AverageStatValue after
3535
* creating or resetting it will be treated as 0 in the average calculation,
3636
* as it will be the base from which the rest of relative values are calculated.
37+
* Therefore, if the values added to an AverageStatValue are relative,
38+
* "hasEnoughData()" will not return true until at least N items were added,
39+
* but if the values are cumulative, it will not return true until at least N+1
40+
* items were added.
3741
*
3842
* Besides the weighted average it is possible to "peek" the last value, either
3943
* the raw value that was added or the relative one after the conversion (which,
@@ -54,15 +58,25 @@ function AverageStatValue(count, type = STAT_VALUE_TYPE.CUMULATIVE, lastValueWei
5458

5559
this._rawValues = []
5660
this._relativeValues = []
61+
62+
this._hasEnoughData = false
5763
}
5864
AverageStatValue.prototype = {
5965

6066
reset() {
6167
this._rawValues = []
6268
this._relativeValues = []
69+
70+
this._hasEnoughData = false
6371
},
6472

6573
add(value) {
74+
if ((this._type === STAT_VALUE_TYPE.CUMULATIVE && this._rawValues.length === this._count)
75+
|| (this._type === STAT_VALUE_TYPE.RELATIVE && this._rawValues.length >= (this._count - 1))
76+
) {
77+
this._hasEnoughData = true
78+
}
79+
6680
if (this._rawValues.length === this._count) {
6781
this._rawValues.shift()
6882
this._relativeValues.shift()
@@ -97,7 +111,7 @@ AverageStatValue.prototype = {
97111
},
98112

99113
hasEnoughData() {
100-
return this._rawValues.length === this._count
114+
return this._hasEnoughData
101115
},
102116

103117
getWeightedAverage() {

src/utils/webrtc/analyzers/AverageStatValue.spec.js

+84-9
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,92 @@ describe('AverageStatValue', () => {
2525
})
2626
})
2727

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

33-
testValues.forEach((val, index) => {
34-
stat.add(val)
35-
expect(stat.hasEnoughData()).toBe(index >= 2)
34+
testValues.forEach((val, index) => {
35+
stat.add(val)
36+
expect(stat.hasEnoughData()).toBe(index >= 3)
3637

37-
stat2.add(val)
38-
expect(stat2.hasEnoughData()).toBe(index >= 2)
38+
stat2.add(val)
39+
expect(stat2.hasEnoughData()).toBe(index >= 2)
40+
})
41+
})
42+
43+
describe('resetting', () => {
44+
let stat
45+
let stat2
46+
47+
const addValues = (values) => {
48+
values.forEach(val => {
49+
stat.add(val)
50+
stat2.add(val)
51+
})
52+
}
53+
54+
beforeEach(() => {
55+
stat = new AverageStatValue(3, STAT_VALUE_TYPE.CUMULATIVE, 3)
56+
stat2 = new AverageStatValue(3, STAT_VALUE_TYPE.RELATIVE, 3)
57+
})
58+
59+
test('before having enough values', () => {
60+
addValues([100, 200])
61+
62+
expect(stat.hasEnoughData()).toBe(false)
63+
expect(stat2.hasEnoughData()).toBe(false)
64+
65+
stat.reset()
66+
stat2.reset()
67+
68+
expect(stat.hasEnoughData()).toBe(false)
69+
expect(stat2.hasEnoughData()).toBe(false)
70+
71+
addValues([150, 123])
72+
73+
expect(stat.hasEnoughData()).toBe(false)
74+
expect(stat2.hasEnoughData()).toBe(false)
75+
76+
addValues([30])
77+
78+
expect(stat.hasEnoughData()).toBe(false)
79+
expect(stat2.hasEnoughData()).toBe(true)
80+
81+
addValues([50])
82+
83+
expect(stat.hasEnoughData()).toBe(true)
84+
expect(stat2.hasEnoughData()).toBe(true)
85+
})
86+
87+
test('after having enough values', () => {
88+
addValues([100, 200, 150, 123])
89+
90+
expect(stat.hasEnoughData()).toBe(true)
91+
expect(stat2.hasEnoughData()).toBe(true)
92+
93+
stat.reset()
94+
stat2.reset()
95+
96+
expect(stat.hasEnoughData()).toBe(false)
97+
expect(stat2.hasEnoughData()).toBe(false)
98+
99+
addValues([30, 50])
100+
101+
expect(stat.hasEnoughData()).toBe(false)
102+
expect(stat2.hasEnoughData()).toBe(false)
103+
104+
addValues([22])
105+
106+
expect(stat.hasEnoughData()).toBe(false)
107+
expect(stat2.hasEnoughData()).toBe(true)
108+
109+
addValues([33])
110+
111+
expect(stat.hasEnoughData()).toBe(true)
112+
expect(stat2.hasEnoughData()).toBe(true)
113+
})
39114
})
40115
})
41116

src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js

+51-10
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,19 @@ PeerConnectionAnalyzer.prototype = {
396396
packetsLost[kind] = this._packetsLost[kind].getLastRawValue()
397397
}
398398

399+
// In some (also strange) cases a newer stat may report a lower
400+
// value than a previous one (it happens sometimes with garbage
401+
// remote reports in simulcast video that cause the values to
402+
// overflow, although it was also seen with a small value regression
403+
// when enabling video). If that happens the stats are reset to
404+
// prevent distorting the analysis with negative packet counts; note
405+
// that in this case the previous value is not kept because it is
406+
// not just an isolated wrong value, all the following stats
407+
// increase from the regressed value.
408+
if (packets[kind] >= 0 && packets[kind] < this._packets[kind].getLastRawValue()) {
409+
this._resetStats(kind)
410+
}
411+
399412
this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind], roundTripTime[kind])
400413
}
401414
},
@@ -466,12 +479,13 @@ PeerConnectionAnalyzer.prototype = {
466479
* The stats reported by the browser can sometimes stall for a second (or
467480
* more, but typically they stall only for a single report). When that
468481
* happens the stats are still reported, but with the same number of packets
469-
* as in the previous report (timestamp and round trip time are updated,
470-
* though). In that case the given stats are not added yet to the average
471-
* stats; they are kept on hold until more stats are provided by the browser
472-
* and it can be determined if the previous stats were stalled or not. If
473-
* they were stalled the previous and new stats are distributed, and if they
474-
* were not they are added as is to the average stats.
482+
* as in the previous report (timestamp and round trip time may be updated
483+
* or not, apparently depending on browser version and/or Janus version). In
484+
* that case the given stats are not added yet to the average stats; they
485+
* are kept on hold until more stats are provided by the browser and it can
486+
* be determined if the previous stats were stalled or not. If they were
487+
* stalled the previous and new stats are distributed, and if they were not
488+
* they are added as is to the average stats.
475489
*
476490
* @param {string} kind the type of the stats ("audio" or "video")
477491
* @param {number} packets the cumulative number of packets
@@ -536,6 +550,18 @@ PeerConnectionAnalyzer.prototype = {
536550
let packetsLostTotal = 0
537551
let timestampsTotal = 0
538552

553+
// If the first timestamp stalled it is assumed that all of them
554+
// stalled and are thus evenly distributed based on the new timestamp.
555+
if (this._stagedTimestamps[kind][0] === timestampsBase) {
556+
const lastTimestamp = this._stagedTimestamps[kind][this._stagedTimestamps[kind].length - 1]
557+
const timestampsTotalDifference = lastTimestamp - timestampsBase
558+
const timestampsDelta = timestampsTotalDifference / this._stagedTimestamps[kind].length
559+
560+
for (let i = 0; i < this._stagedTimestamps[kind].length - 1; i++) {
561+
this._stagedTimestamps[kind][i] += timestampsDelta * (i + 1)
562+
}
563+
}
564+
539565
for (let i = 0; i < this._stagedPackets[kind].length; i++) {
540566
packetsTotal += (this._stagedPackets[kind][i] - packetsBase)
541567
packetsBase = this._stagedPackets[kind][i]
@@ -562,7 +588,11 @@ PeerConnectionAnalyzer.prototype = {
562588
packetsLostBase = this._stagedPacketsLost[kind][i]
563589

564590
// Timestamps and round trip time are not distributed, as those
565-
// values are properly updated even if the stats are stalled.
591+
// values may be properly updated even if the stats are stalled. In
592+
// case they were not timestamps were already evenly distributed
593+
// above, and round trip time can not be distributed, as it is
594+
// already provided in the stats as a relative value rather than a
595+
// cumulative one.
566596
}
567597
},
568598

@@ -612,11 +642,19 @@ PeerConnectionAnalyzer.prototype = {
612642
},
613643

614644
_calculateConnectionQuality(kind) {
645+
const packets = this._packets[kind]
646+
const packetsLost = this._packetsLost[kind]
647+
const timestamps = this._timestamps[kind]
615648
const packetsLostRatio = this._packetsLostRatio[kind]
616649
const packetsPerSecond = this._packetsPerSecond[kind]
617650
const roundTripTime = this._roundTripTime[kind]
618651

619-
if (!packetsLostRatio.hasEnoughData() || !packetsPerSecond.hasEnoughData()) {
652+
// packetsLostRatio and packetsPerSecond are relative values, but they
653+
// are calculated from cumulative values. Therefore, it is necessary to
654+
// check if the cumulative values that are their source have enough data
655+
// or not, rather than checking if the relative values themselves have
656+
// enough data.
657+
if (!packets.hasEnoughData() || !packetsLost.hasEnoughData() || !timestamps.hasEnoughData()) {
620658
return CONNECTION_QUALITY.UNKNOWN
621659
}
622660

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

664705
if (packetsLostRatioWeightedAverage > 0.3) {

0 commit comments

Comments
 (0)