Skip to content

Commit 233f05b

Browse files
authored
Merge pull request #14250 from nextcloud/backport/14095/stable29
[stable29] Fix (known) false positives in connection warning
2 parents 27e2264 + b701427 commit 233f05b

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
@@ -36,7 +36,7 @@ const STAT_VALUE_TYPE = {
3636
*
3737
* The number of items to keep track of must be set when the AverageStatValue is
3838
* created. Once N items have been added adding a new one will discard the
39-
* oldest value. "hasEnoughData()" can be used to check if at least N items have
39+
* oldest value. "hasEnoughData()" can be used to check if enough items have
4040
* been added already and the average is reliable.
4141
*
4242
* An RTCStatsReport value can be cumulative since the creation of the
@@ -50,6 +50,10 @@ const STAT_VALUE_TYPE = {
5050
* however, that the first value added to a cumulative AverageStatValue after
5151
* creating or resetting it will be treated as 0 in the average calculation,
5252
* as it will be the base from which the rest of relative values are calculated.
53+
* Therefore, if the values added to an AverageStatValue are relative,
54+
* "hasEnoughData()" will not return true until at least N items were added,
55+
* but if the values are cumulative, it will not return true until at least N+1
56+
* items were added.
5357
*
5458
* Besides the weighted average it is possible to "peek" the last value, either
5559
* the raw value that was added or the relative one after the conversion (which,
@@ -70,15 +74,25 @@ function AverageStatValue(count, type = STAT_VALUE_TYPE.CUMULATIVE, lastValueWei
7074

7175
this._rawValues = []
7276
this._relativeValues = []
77+
78+
this._hasEnoughData = false
7379
}
7480
AverageStatValue.prototype = {
7581

7682
reset() {
7783
this._rawValues = []
7884
this._relativeValues = []
85+
86+
this._hasEnoughData = false
7987
},
8088

8189
add(value) {
90+
if ((this._type === STAT_VALUE_TYPE.CUMULATIVE && this._rawValues.length === this._count)
91+
|| (this._type === STAT_VALUE_TYPE.RELATIVE && this._rawValues.length >= (this._count - 1))
92+
) {
93+
this._hasEnoughData = true
94+
}
95+
8296
if (this._rawValues.length === this._count) {
8397
this._rawValues.shift()
8498
this._relativeValues.shift()
@@ -113,7 +127,7 @@ AverageStatValue.prototype = {
113127
},
114128

115129
hasEnoughData() {
116-
return this._rawValues.length === this._count
130+
return this._hasEnoughData
117131
},
118132

119133
getWeightedAverage() {

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

+84-9
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,92 @@ describe('AverageStatValue', () => {
2121
})
2222
})
2323

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

29-
testValues.forEach((val, index) => {
30-
stat.add(val)
31-
expect(stat.hasEnoughData()).toBe(index >= 2)
30+
testValues.forEach((val, index) => {
31+
stat.add(val)
32+
expect(stat.hasEnoughData()).toBe(index >= 3)
3233

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

src/utils/webrtc/analyzers/PeerConnectionAnalyzer.js

+51-10
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,19 @@ PeerConnectionAnalyzer.prototype = {
412412
packetsLost[kind] = this._packetsLost[kind].getLastRawValue()
413413
}
414414

415+
// In some (also strange) cases a newer stat may report a lower
416+
// value than a previous one (it happens sometimes with garbage
417+
// remote reports in simulcast video that cause the values to
418+
// overflow, although it was also seen with a small value regression
419+
// when enabling video). If that happens the stats are reset to
420+
// prevent distorting the analysis with negative packet counts; note
421+
// that in this case the previous value is not kept because it is
422+
// not just an isolated wrong value, all the following stats
423+
// increase from the regressed value.
424+
if (packets[kind] >= 0 && packets[kind] < this._packets[kind].getLastRawValue()) {
425+
this._resetStats(kind)
426+
}
427+
415428
this._addStats(kind, packets[kind], packetsLost[kind], timestamp[kind], roundTripTime[kind])
416429
}
417430
},
@@ -482,12 +495,13 @@ PeerConnectionAnalyzer.prototype = {
482495
* The stats reported by the browser can sometimes stall for a second (or
483496
* more, but typically they stall only for a single report). When that
484497
* happens the stats are still reported, but with the same number of packets
485-
* as in the previous report (timestamp and round trip time are updated,
486-
* though). In that case the given stats are not added yet to the average
487-
* stats; they are kept on hold until more stats are provided by the browser
488-
* and it can be determined if the previous stats were stalled or not. If
489-
* they were stalled the previous and new stats are distributed, and if they
490-
* were not they are added as is to the average stats.
498+
* as in the previous report (timestamp and round trip time may be updated
499+
* or not, apparently depending on browser version and/or Janus version). In
500+
* that case the given stats are not added yet to the average stats; they
501+
* are kept on hold until more stats are provided by the browser and it can
502+
* be determined if the previous stats were stalled or not. If they were
503+
* stalled the previous and new stats are distributed, and if they were not
504+
* they are added as is to the average stats.
491505
*
492506
* @param {string} kind the type of the stats ("audio" or "video")
493507
* @param {number} packets the cumulative number of packets
@@ -552,6 +566,18 @@ PeerConnectionAnalyzer.prototype = {
552566
let packetsLostTotal = 0
553567
let timestampsTotal = 0
554568

569+
// If the first timestamp stalled it is assumed that all of them
570+
// stalled and are thus evenly distributed based on the new timestamp.
571+
if (this._stagedTimestamps[kind][0] === timestampsBase) {
572+
const lastTimestamp = this._stagedTimestamps[kind][this._stagedTimestamps[kind].length - 1]
573+
const timestampsTotalDifference = lastTimestamp - timestampsBase
574+
const timestampsDelta = timestampsTotalDifference / this._stagedTimestamps[kind].length
575+
576+
for (let i = 0; i < this._stagedTimestamps[kind].length - 1; i++) {
577+
this._stagedTimestamps[kind][i] += timestampsDelta * (i + 1)
578+
}
579+
}
580+
555581
for (let i = 0; i < this._stagedPackets[kind].length; i++) {
556582
packetsTotal += (this._stagedPackets[kind][i] - packetsBase)
557583
packetsBase = this._stagedPackets[kind][i]
@@ -578,7 +604,11 @@ PeerConnectionAnalyzer.prototype = {
578604
packetsLostBase = this._stagedPacketsLost[kind][i]
579605

580606
// Timestamps and round trip time are not distributed, as those
581-
// values are properly updated even if the stats are stalled.
607+
// values may be properly updated even if the stats are stalled. In
608+
// case they were not timestamps were already evenly distributed
609+
// above, and round trip time can not be distributed, as it is
610+
// already provided in the stats as a relative value rather than a
611+
// cumulative one.
582612
}
583613
},
584614

@@ -628,11 +658,19 @@ PeerConnectionAnalyzer.prototype = {
628658
},
629659

630660
_calculateConnectionQuality(kind) {
661+
const packets = this._packets[kind]
662+
const packetsLost = this._packetsLost[kind]
663+
const timestamps = this._timestamps[kind]
631664
const packetsLostRatio = this._packetsLostRatio[kind]
632665
const packetsPerSecond = this._packetsPerSecond[kind]
633666
const roundTripTime = this._roundTripTime[kind]
634667

635-
if (!packetsLostRatio.hasEnoughData() || !packetsPerSecond.hasEnoughData()) {
668+
// packetsLostRatio and packetsPerSecond are relative values, but they
669+
// are calculated from cumulative values. Therefore, it is necessary to
670+
// check if the cumulative values that are their source have enough data
671+
// or not, rather than checking if the relative values themselves have
672+
// enough data.
673+
if (!packets.hasEnoughData() || !packetsLost.hasEnoughData() || !timestamps.hasEnoughData()) {
636674
return CONNECTION_QUALITY.UNKNOWN
637675
}
638676

@@ -671,10 +709,13 @@ PeerConnectionAnalyzer.prototype = {
671709
// quality to keep a smooth video, albeit on a lower resolution. Thus
672710
// with a threshold of 10 packets issues can be detected too for videos,
673711
// although only once they can not be further downscaled.
712+
// Despite all of the above it has been observed that less than 10
713+
// packets are sometimes sent without any connection problem (for
714+
// example, when the background is blurred and the video quality is
715+
// reduced due to being in a call with several participants), so for now
716+
// it is only logged but not reported.
674717
if (packetsPerSecond.getWeightedAverage() < 10) {
675718
this._logStats(kind, 'Low packets per second: ' + packetsPerSecond.getWeightedAverage())
676-
677-
return CONNECTION_QUALITY.VERY_BAD
678719
}
679720

680721
if (packetsLostRatioWeightedAverage > 0.3) {

0 commit comments

Comments
 (0)