소스 검색

feat: Reworks the connection quality calculation.

Cleans up code. Paces the increase of connection quality. Take ramp-up
into account. Uses sending bitrate by default whenever possible (with
and without simulcast, as long as we know the sending bitrate and
resolution).

Updates the packet loss based calculation (used whenever we don't know
the sending bitrate and input resolution) with hard-coded thresholds,
so that it doesn't scale linearly (15% packet loss doesn't yield 85%
connection quality).
master
Boris Grozev 8 년 전
부모
커밋
62f4922201
1개의 변경된 파일285개의 추가작업 그리고 101개의 파일을 삭제
  1. 285
    101
      modules/connectivity/ConnectionQuality.js

+ 285
- 101
modules/connectivity/ConnectionQuality.js 파일 보기

@@ -2,9 +2,13 @@ import * as ConnectionQualityEvents
2 2
     from "../../service/connectivity/ConnectionQualityEvents";
3 3
 import * as ConferenceEvents from "../../JitsiConferenceEvents";
4 4
 import {getLogger} from "jitsi-meet-logger";
5
+import RTCBrowserType from "../RTC/RTCBrowserType";
5 6
 
7
+var XMPPEvents = require('../../service/xmpp/XMPPEvents');
6 8
 var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
7 9
 var MediaType = require('../../service/RTC/MediaType');
10
+var VideoType = require('../../service/RTC/VideoType');
11
+var Resolutions = require("../../service/RTC/Resolutions");
8 12
 
9 13
 const logger = getLogger(__filename);
10 14
 
@@ -14,53 +18,89 @@ const logger = getLogger(__filename);
14 18
  */
15 19
 const STATS_MESSAGE_TYPE = "stats";
16 20
 
17
-// webrtc table describing simulcast resolutions and used bandwidth
18
-// https://chromium.googlesource.com/external/webrtc/+/master/webrtc/media/engine/simulcast.cc#42
19
-const _bandwidthMap = [
20
-    { width: 1920, height: 1080, layers:3, max: 5000, min: 800 },
21
-    { width: 1280, height: 720,  layers:3, max: 2500, min: 600 },
22
-    { width: 960,  height: 540,  layers:3, max: 900,  min: 450 },
23
-    { width: 640,  height: 360,  layers:2, max: 700,  min: 150 },
24
-    { width: 480,  height: 270,  layers:2, max: 450,  min: 150 },
25
-    { width: 320,  height: 180,  layers:1, max: 200,  min: 30 }
21
+/**
22
+ * See media/engine/simulcast.ss from webrtc.org
23
+ */
24
+const kSimulcastFormats = [
25
+    { width: 1920, height: 1080, layers:3, max: 5000, target: 4000, min: 800 },
26
+    { width: 1280, height: 720,  layers:3, max: 2500, target: 2500, min: 600 },
27
+    { width: 960,  height: 540,  layers:3, max: 900,  target: 900, min: 450 },
28
+    { width: 640,  height: 360,  layers:2, max: 700,  target: 500, min: 150 },
29
+    { width: 480,  height: 270,  layers:2, max: 450,  target: 350, min: 150 },
30
+    { width: 320,  height: 180,  layers:1, max: 200,  target: 150, min: 30 }
26 31
 ];
27 32
 
28 33
 /**
29
- * Calculates the quality percent based on passed new and old value.
30
- * @param newVal the new value
31
- * @param oldVal the old value
34
+ * The initial bitrate for video in kbps.
32 35
  */
33
-function calculateQuality(newVal, oldVal) {
34
-    return (newVal <= oldVal) ? newVal : (9*oldVal + newVal) / 10;
35
-}
36
+var startBitrate = 800;
36 37
 
37 38
 /**
38
- * Calculates the quality percentage based on the input resolution height and
39
- * the upload reported by the client. The value is based on the interval from
40
- * _bandwidthMap.
41
- * @param inputHeight the resolution used to open the camera.
42
- * @param upload the upload rate reported by client.
43
- * @returns {int} the percent of upload based on _bandwidthMap and maximum value
44
- * of 100, as values of the map are approximate and clients can stream above
45
- * those values. Returns undefined if no result is found.
39
+ * Gets the expected bitrate (in kbps) in perfect network conditions.
40
+ * @param simulcast {boolean} whether simulcast is enabled or not.
41
+ * @param resolution {Resolution} the resolution.
42
+ * @param millisSinceStart {number} the number of milliseconds since sending
43
+ * video started.
46 44
  */
47
-function calculateQualityUsingUpload(inputHeight, upload) {
48
-    // found resolution from _bandwidthMap which height is equal or less than
49
-    // the inputHeight
50
-    let foundResolution = _bandwidthMap.find((r) => (r.height <= inputHeight));
45
+function getTarget(simulcast, resolution, millisSinceStart) {
46
+    let target = 0;
47
+    let height = Math.min(resolution.height, resolution.width);
51 48
 
52
-    if (!foundResolution)
53
-        return undefined;
49
+    if (simulcast) {
50
+        // Find the first format with height no bigger than ours.
51
+        let simulcastFormat = kSimulcastFormats.find(f => f.height <= height);
52
+        if (simulcastFormat) {
53
+            // Sum the target fields from all simulcast layers for the given
54
+            // resolution (e.g. 720p + 360p + 180p).
55
+            for (height = simulcastFormat.height; height >= 180; height /=2) {
56
+                simulcastFormat
57
+                    = kSimulcastFormats.find(f => f.height == height);
58
+                if (simulcastFormat) {
59
+                    target += simulcastFormat.target;
60
+                } else {
61
+                    break;
62
+                }
63
+            }
64
+        }
65
+    } else {
66
+        // See GetMaxDefaultVideoBitrateKbps in
67
+        // media/engine/webrtcvideoengine2.cc from webrtc.org
68
+        let pixels = resolution.width * resolution.height;
69
+        if (pixels <= 320 * 240) {
70
+            target = 600;
71
+        } else if (pixels <= 640 * 480) {
72
+            target =  1700;
73
+        } else if (pixels <= 960 * 540) {
74
+            target = 2000;
75
+        } else {
76
+            target = 2500;
77
+        }
78
+    }
54 79
 
55
-    if (upload <= foundResolution.min)
56
-        return 0;
80
+    // Allow for an additional 3 seconds for ramp up -- delay any initial drop
81
+    // of connection quality by 3 seconds.
82
+    return Math.min(target, rampUp(Math.max(0, millisSinceStart - 3000)));
83
+}
57 84
 
58
-    return Math.min(
59
-        ((upload - foundResolution.min)*100)
60
-            / (foundResolution.max - foundResolution.min),
61
-        100);
85
+/**
86
+ * Gets the bitrate to which GCC would have ramped up in perfect network
87
+ * conditions after millisSinceStart milliseconds.
88
+ * @param millisSinceStart {number} the number of milliseconds since sending
89
+ * video was enabled.
90
+ */
91
+function rampUp(millisSinceStart) {
92
+    // According to GCC the send side bandwidth estimation grows with at most
93
+    // 8% per second.
94
+    // https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02#section-5.5
95
+    return startBitrate * Math.pow(1.08, millisSinceStart / 1000);
62 96
 }
63 97
 
98
+/**
99
+ * A class which monitors the local statistics coming from the RTC modules, and
100
+ * calculates a "connection quality" value, in percent, for the media
101
+ * connection. A value of 100% indicates a very good network connection, and a
102
+ * value of 0% indicates a poor connection.
103
+ */
64 104
 export default class ConnectionQuality {
65 105
     constructor(conference, eventEmitter, options) {
66 106
         this.eventEmitter = eventEmitter;
@@ -68,25 +108,72 @@ export default class ConnectionQuality {
68 108
         /**
69 109
          * The owning JitsiConference.
70 110
          */
71
-        this.conference = conference;
111
+        this._conference = conference;
112
+
113
+        /**
114
+         * Whether simulcast is supported. Note that even if supported, it is
115
+         * currently not used for screensharing, which is why we have an
116
+         * additional check.
117
+         */
118
+        this._simulcast
119
+            = !options.disableSimulcast && RTCBrowserType.supportsSimulcast();
72 120
 
73
-        this.disableQualityBasedOnBandwidth =
74
-            options.forceQualityBasedOnBandwidth
75
-                    ? false : !!options.disableSimulcast;
76 121
         /**
77 122
          * Holds statistics about the local connection quality.
78 123
          */
79
-        this.localStats = {connectionQuality: 100};
124
+        this._localStats = {connectionQuality: 100};
125
+
126
+        /**
127
+         * The time this._localStats.connectionQuality was last updated.
128
+         */
129
+        this._lastConnectionQualityUpdate = -1;
80 130
 
81 131
         /**
82 132
          * Maps a participant ID to an object holding connection quality
83 133
          * statistics received from this participant.
84 134
          */
85
-        this.remoteStats = {};
135
+        this._remoteStats = {};
136
+
137
+        /**
138
+         * The time that the ICE state last changed to CONNECTED. We use this
139
+         * to calculate how much time we as a sender have had to ramp-up.
140
+         */
141
+        this._timeIceConnected = -1;
142
+
143
+        /**
144
+         * The time that local video was unmuted. We use this to calculate how
145
+         * much time we as a sender have had to ramp-up.
146
+         */
147
+        this._timeVideoUnmuted = -1;
86 148
 
87
-        conference.on(ConferenceEvents.CONNECTION_INTERRUPTED,
88
-                      () => { this._updateLocalConnectionQuality(0); });
89 149
 
150
+        // We assume a global startBitrate value for the sake of simplicity.
151
+        if (options.startBitrate && options.startBitrate > 0) {
152
+            startBitrate = options.startBitrate;
153
+        }
154
+
155
+        // TODO: consider ignoring these events and letting the user of
156
+        // lib-jitsi-meet handle these separately.
157
+        conference.on(
158
+            ConferenceEvents.CONNECTION_INTERRUPTED,
159
+            () => {
160
+                this._updateLocalConnectionQuality(0);
161
+                this.eventEmitter.emit(
162
+                    ConnectionQualityEvents.LOCAL_STATS_UPDATED,
163
+                    this._localStats);
164
+                this._broadcastLocalStats();
165
+            });
166
+
167
+        conference.room.addListener(
168
+            XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
169
+            (newState) => {
170
+                if (newState === 'connected') {
171
+                    this._timeIceConnected = window.performance.now();
172
+                }
173
+            });
174
+
175
+        // Listen to DataChannel message from other participants in the
176
+        // conference, and update the _remoteStats field accordingly.
90 177
         conference.on(
91 178
             ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
92 179
             (participant, payload) => {
@@ -96,48 +183,140 @@ export default class ConnectionQuality {
96 183
                 }
97 184
         });
98 185
 
186
+        // Listen to local statistics events originating from the RTC module
187
+        // and update the _localStats field.
188
+        // Oh, and by the way, the resolutions of all remote participants are
189
+        // also piggy-backed in these "local" statistics. It's obvious, really,
190
+        // if one carefully reads the *code* (but not the docs) in
191
+        // UI/VideoLayout/VideoLayout.js#updateLocalConnectionStats in
192
+        // jitsi-meet
193
+        // TODO: We should keep track of the remote resolution in _remoteStats,
194
+        // and notify about changes via separate events.
99 195
         conference.on(
100 196
             ConferenceEvents.CONNECTION_STATS,
101 197
             this._updateLocalStats.bind(this));
198
+
199
+        // Save the last time we were unmuted.
200
+        conference.on(
201
+            ConferenceEvents.TRACK_MUTE_CHANGED,
202
+            (track) => {
203
+                if (track.isVideoTrack()) {
204
+                    if (track.isMuted()) {
205
+                        this._timeVideoUnmuted = -1;
206
+                    } else {
207
+                        this._maybeUpdateUnmuteTime();
208
+                    }
209
+                }
210
+            });
211
+        conference.on(
212
+            ConferenceEvents.TRACK_ADDED,
213
+            (track) => {
214
+                if (track.isVideoTrack() && !track.isMuted())
215
+                {
216
+                    this._maybeUpdateUnmuteTime();
217
+                }
218
+            });
102 219
     }
103 220
 
104 221
     /**
105
-     * Returns the new quality value based on the input parameters.
106
-     * Used to calculate remote and local values.
107
-     * @param data the data
108
-     * @param lastQualityValue the last value we calculated
109
-     * @param videoType need to check whether we are screen sharing
110
-     * @param isMuted is video muted
111
-     * @param resolution the input resolution used by the camera
112
-     * @returns {*} the newly calculated value or undefined if no result
113
-     * @private
222
+     * Sets _timeVideoUnmuted if it was previously unset. If it was already set,
223
+     * doesn't change it.
114 224
      */
115
-    _getNewQualityValue(
116
-        data, lastQualityValue, videoType, isMuted, resolution) {
117
-        if (this.disableQualityBasedOnBandwidth
118
-            || isMuted
119
-            || videoType === 'desktop'
120
-            || !resolution) {
121
-            return calculateQuality(
122
-                100 - data.packetLoss.total,
123
-                lastQualityValue || 100);
225
+    _maybeUpdateUnmuteTime() {
226
+        if (this._timeVideoUnmuted < 0) {
227
+            this._timeVideoUnmuted = window.performance.now();
228
+        }
229
+    }
230
+
231
+    /**
232
+     * Calculates a new "connection quality" value.
233
+     * @param videoType {VideoType} the type of the video source (camera or
234
+     * a screen capture).
235
+     * @param isMuted {boolean} whether the local video is muted.
236
+     * @param resolutionName {Resolution} the input resolution used by the
237
+     * camera.
238
+     * @returns {*} the newly calculated connection quality.
239
+     */
240
+    _calculateConnectionQuality(videoType, isMuted, resolutionName) {
241
+
242
+        // resolutionName is an index into Resolutions (where "720" is
243
+        // "1280x720" and "960" is "960x720" ...).
244
+        let resolution = Resolutions[resolutionName];
245
+
246
+        let quality = 100;
247
+
248
+        if (isMuted || !resolution
249
+            || this._timeIceConnected < 0
250
+            || this._timeVideoUnmuted < 0) {
251
+
252
+            // Calculate a value based on packet loss only.
253
+            if (!this._localStats.packetLoss
254
+                || this._localStats.packetLoss.total === undefined) {
255
+                logger.error("Cannot calculate connection quality, unknown "
256
+                    + "packet loss.");
257
+                quality = 100;
258
+            } else {
259
+                let loss = this._localStats.packetLoss.total;
260
+                if (loss <= 2) {
261
+                    quality = 100;
262
+                } else if (loss <= 4) {
263
+                    quality = 70; // 4 bars
264
+                } else if (loss <= 6) {
265
+                    quality = 50; // 3 bars
266
+                } else if (loss <= 8) {
267
+                    quality = 30; // 2 bars
268
+                } else if (loss <= 12) {
269
+                    quality = 10; // 1 bars
270
+                } else {
271
+                    quality = 0; // Still 1 bar, but slower climb-up.
272
+                }
273
+            }
124 274
         } else {
125
-            return calculateQualityUsingUpload(
126
-                resolution,
127
-                data.bitrate.upload);
275
+            // Calculate a value based on the sending bitrate.
276
+
277
+            // simulcast is not used for screensharing.
278
+            let simulcast = (this._simulcast && videoType === VideoType.CAMERA);
279
+
280
+            // time since sending of video was enabled.
281
+            let millisSinceStart = window.performance.now()
282
+                    - Math.max(this._timeVideoUnmuted, this._timeIceConnected);
283
+
284
+            // expected sending bitrate in perfect conditions
285
+            let target = getTarget(simulcast, resolution, millisSinceStart);
286
+            target = 0.9 * target;
287
+
288
+            quality = 100 * this._localStats.bitrate.upload / target;
289
+
290
+            // Whatever the bitrate, drop early if there is significant loss
291
+            if (this._localStats.packetLoss
292
+                && this._localStats.packetLoss.total >= 10) {
293
+                quality = Math.min(quality, 30);
294
+            }
295
+        }
296
+
297
+        // Make sure that the quality doesn't climb quickly
298
+        if (this._lastConnectionQualityUpdate > 0)
299
+        {
300
+            let maxIncreasePerSecond = 2;
301
+            let prevConnectionQuality = this._localStats.connectionQuality;
302
+            let diffSeconds
303
+                = (window.performance.now()
304
+                    - this._lastConnectionQualityUpdate) / 1000;
305
+            quality = Math.min(
306
+                quality,
307
+                prevConnectionQuality + diffSeconds * maxIncreasePerSecond);
128 308
         }
309
+
310
+        return Math.min(100, quality);
129 311
     }
130 312
 
131 313
     /**
132
-     * Updates only the localConnectionQuality value
133
-     * @param values {int} the new value. should be from 0 - 100.
314
+     * Updates the localConnectionQuality value
315
+     * @param values {number} the new value. Should be in [0, 100].
134 316
      */
135 317
     _updateLocalConnectionQuality(value) {
136
-        this.localStats.connectionQuality = value;
137
-        this.eventEmitter.emit(
138
-            ConnectionQualityEvents.LOCAL_STATS_UPDATED,
139
-            this.localStats);
140
-        this._broadcastLocalStats();
318
+        this._localStats.connectionQuality = value;
319
+        this._lastConnectionQualityUpdate = window.performance.now();
141 320
     }
142 321
 
143 322
     /**
@@ -147,20 +326,23 @@ export default class ConnectionQuality {
147 326
     _broadcastLocalStats() {
148 327
         // Send only the data that remote participants care about.
149 328
         let data = {
150
-            bitrate: this.localStats.bitrate,
151
-            packetLoss: this.localStats.packetLoss,
152
-            connectionQuality: this.localStats.connectionQuality
329
+            bitrate: this._localStats.bitrate,
330
+            packetLoss: this._localStats.packetLoss,
331
+            connectionQuality: this._localStats.connectionQuality
153 332
         };
154 333
 
334
+        // TODO: It looks like the remote participants don't really "care"
335
+        // about the resolution, and they look at their local rendered
336
+        // resolution instead. Consider removing this.
155 337
         let localVideoTrack
156
-            = this.conference.getLocalTracks(MediaType.VIDEO)
338
+            = this._conference.getLocalTracks(MediaType.VIDEO)
157 339
                 .find(track => track.isVideoTrack());
158 340
         if (localVideoTrack && localVideoTrack.resolution) {
159 341
             data.resolution = localVideoTrack.resolution;
160 342
         }
161 343
 
162 344
         try {
163
-            this.conference.broadcastEndpointMessage({
345
+            this._conference.broadcastEndpointMessage({
164 346
                 type: STATS_MESSAGE_TYPE,
165 347
                 values: data });
166 348
         } catch (e) {
@@ -174,39 +356,41 @@ export default class ConnectionQuality {
174 356
     /**
175 357
      * Updates the local statistics
176 358
      * @param data new statistics
177
-     * @param updateLocalConnectionQuality {boolean} weather to recalculate
178
-     * localConnectionQuality or not.
179
-     * @param videoType the local video type
180
-     * @param isMuted current state of local video, whether it is muted
181
-     * @param resolution the current resolution used by local video
182 359
      */
183 360
     _updateLocalStats(data) {
184
-
361
+        let key;
185 362
         let updateLocalConnectionQuality
186
-            = !this.conference.isConnectionInterrupted();
363
+            = !this._conference.isConnectionInterrupted();
187 364
         let localVideoTrack =
188
-                this.conference.getLocalTracks(MediaType.VIDEO)
365
+                this._conference.getLocalTracks(MediaType.VIDEO)
189 366
                     .find(track => track.isVideoTrack());
190 367
         let videoType = localVideoTrack ? localVideoTrack.videoType : undefined;
191 368
         let isMuted = localVideoTrack ? localVideoTrack.isMuted() : true;
192 369
         let resolution = localVideoTrack ? localVideoTrack.resolution : null;
193
-        let prevConnectionQuality = this.localStats.connectionQuality || 0;
194
-
195
-        this.localStats = data;
196
-        if(updateLocalConnectionQuality) {
197
-            let val = this._getNewQualityValue(
198
-                this.localStats,
199
-                prevConnectionQuality,
200
-                videoType,
201
-                isMuted,
202
-                resolution);
203
-            if (val !== undefined) {
204
-                this.localStats.connectionQuality = val;
370
+
371
+        if (!isMuted) {
372
+            this._maybeUpdateUnmuteTime();
373
+        }
374
+
375
+        // Copy the fields already in 'data'.
376
+        for (key in data) {
377
+            if (data.hasOwnProperty(key)) {
378
+                this._localStats[key] = data[key];
205 379
             }
206 380
         }
381
+
382
+        // And re-calculate the connectionQuality field.
383
+        if (updateLocalConnectionQuality) {
384
+            this._updateLocalConnectionQuality(
385
+                this._calculateConnectionQuality(
386
+                    videoType,
387
+                    isMuted,
388
+                    resolution));
389
+        }
390
+
207 391
         this.eventEmitter.emit(
208 392
             ConnectionQualityEvents.LOCAL_STATS_UPDATED,
209
-            this.localStats);
393
+            this._localStats);
210 394
         this._broadcastLocalStats();
211 395
     }
212 396
 
@@ -214,11 +398,10 @@ export default class ConnectionQuality {
214 398
      * Updates remote statistics
215 399
      * @param id the id of the remote participant
216 400
      * @param data the statistics received
217
-     * @param isRemoteVideoMuted whether remote video is muted
218 401
      */
219 402
     _updateRemoteStats(id, data) {
220 403
             // Use only the fields we need
221
-            this.remoteStats[id] = {
404
+            this._remoteStats[id] = {
222 405
                 bitrate: data.bitrate,
223 406
                 packetLoss: data.packetLoss,
224 407
                 connectionQuality: data.connectionQuality
@@ -227,13 +410,14 @@ export default class ConnectionQuality {
227 410
             this.eventEmitter.emit(
228 411
                 ConnectionQualityEvents.REMOTE_STATS_UPDATED,
229 412
                 id,
230
-                this.remoteStats[id]);
413
+                this._remoteStats[id]);
231 414
     }
232 415
 
233 416
     /**
234 417
      * Returns the local statistics.
418
+     * Exported only for use in jitsi-meet-torture.
235 419
      */
236 420
     getStats() {
237
-        return this.localStats;
421
+        return this._localStats;
238 422
     }
239 423
 }

Loading…
취소
저장