|
@@ -0,0 +1,213 @@
|
|
1
|
+import * as ConnectionQualityEvents
|
|
2
|
+ from "../../service/connectivity/ConnectionQualityEvents";
|
|
3
|
+import * as ConferenceEvents from "../../JitsiConferenceEvents";
|
|
4
|
+
|
|
5
|
+// webrtc table describing simulcast resolutions and used bandwidth
|
|
6
|
+// https://chromium.googlesource.com/external/webrtc/+/master/webrtc/media/engine/simulcast.cc#42
|
|
7
|
+const _bandwidthMap = [
|
|
8
|
+ { width: 1920, height: 1080, layers:3, max: 5000, min: 800 },
|
|
9
|
+ { width: 1280, height: 720, layers:3, max: 2500, min: 600 },
|
|
10
|
+ { width: 960, height: 540, layers:3, max: 900, min: 450 },
|
|
11
|
+ { width: 640, height: 360, layers:2, max: 700, min: 150 },
|
|
12
|
+ { width: 480, height: 270, layers:2, max: 450, min: 150 },
|
|
13
|
+ { width: 320, height: 180, layers:1, max: 200, min: 30 }
|
|
14
|
+];
|
|
15
|
+
|
|
16
|
+/**
|
|
17
|
+ * Calculates the quality percent based on passed new and old value.
|
|
18
|
+ * @param newVal the new value
|
|
19
|
+ * @param oldVal the old value
|
|
20
|
+ */
|
|
21
|
+function calculateQuality(newVal, oldVal) {
|
|
22
|
+ return (newVal <= oldVal) ? newVal : (9*oldVal + newVal) / 10;
|
|
23
|
+}
|
|
24
|
+
|
|
25
|
+/**
|
|
26
|
+ * Calculates the quality percentage based on the input resolution height and
|
|
27
|
+ * the upload reported by the client. The value is based on the interval from
|
|
28
|
+ * _bandwidthMap.
|
|
29
|
+ * @param inputHeight the resolution used to open the camera.
|
|
30
|
+ * @param upload the upload rate reported by client.
|
|
31
|
+ * @returns {int} the percent of upload based on _bandwidthMap and maximum value
|
|
32
|
+ * of 100, as values of the map are approximate and clients can stream above
|
|
33
|
+ * those values. Returns undefined if no result is found.
|
|
34
|
+ */
|
|
35
|
+function calculateQualityUsingUpload(inputHeight, upload) {
|
|
36
|
+ // found resolution from _bandwidthMap which height is equal or less than
|
|
37
|
+ // the inputHeight
|
|
38
|
+ let foundResolution = _bandwidthMap.find((r) => (r.height <= inputHeight));
|
|
39
|
+
|
|
40
|
+ if (!foundResolution)
|
|
41
|
+ return undefined;
|
|
42
|
+
|
|
43
|
+ if (upload <= foundResolution.min)
|
|
44
|
+ return 0;
|
|
45
|
+
|
|
46
|
+ return Math.min(
|
|
47
|
+ ((upload - foundResolution.min)*100)
|
|
48
|
+ / (foundResolution.max - foundResolution.min),
|
|
49
|
+ 100);
|
|
50
|
+}
|
|
51
|
+
|
|
52
|
+export default class ConnectionQuality {
|
|
53
|
+ constructor(conference, eventEmitter, options) {
|
|
54
|
+ this.eventEmitter = eventEmitter;
|
|
55
|
+
|
|
56
|
+ this.disableQualityBasedOnBandwidth =
|
|
57
|
+ options.forceQualityBasedOnBandwidth
|
|
58
|
+ ? false : !!options.disableSimulcast;
|
|
59
|
+ /**
|
|
60
|
+ * local stats
|
|
61
|
+ * @type {{}}
|
|
62
|
+ */
|
|
63
|
+ this.localStats = {};
|
|
64
|
+
|
|
65
|
+ /**
|
|
66
|
+ * remote stats
|
|
67
|
+ * @type {{}}
|
|
68
|
+ */
|
|
69
|
+ this.remoteStats = {};
|
|
70
|
+
|
|
71
|
+ /**
|
|
72
|
+ * Quality percent( 100% - good, 0% - bad.) for the local user.
|
|
73
|
+ */
|
|
74
|
+ this.localConnectionQuality = 100;
|
|
75
|
+
|
|
76
|
+ /**
|
|
77
|
+ * Quality percent( 100% - good, 0% - bad.) stored per id.
|
|
78
|
+ */
|
|
79
|
+ this.remoteConnectionQuality = {};
|
|
80
|
+
|
|
81
|
+ conference.on(ConferenceEvents.CONNECTION_INTERRUPTED,
|
|
82
|
+ () => { this._updateLocalConnectionQuality(0); });
|
|
83
|
+
|
|
84
|
+ conference.on(ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
|
85
|
+ (participant, payload) => {
|
|
86
|
+ // TODO move "stats" to a constant.
|
|
87
|
+ if(payload.type === "stats") {
|
|
88
|
+ let remoteVideo = participant.getTracks()
|
|
89
|
+ .find(tr => tr.isVideoTrack());
|
|
90
|
+ this.updateRemoteStats(
|
|
91
|
+ participant.getId(),
|
|
92
|
+ payload.values,
|
|
93
|
+ remoteVideo ? remoteVideo.videoType : undefined,
|
|
94
|
+ remoteVideo ? remoteVideo.isMuted() : undefined);
|
|
95
|
+ }
|
|
96
|
+ });
|
|
97
|
+ }
|
|
98
|
+
|
|
99
|
+ /**
|
|
100
|
+ * Returns the new quality value based on the input parameters.
|
|
101
|
+ * Used to calculate remote and local values.
|
|
102
|
+ * @param data the data
|
|
103
|
+ * @param lastQualityValue the last value we calculated
|
|
104
|
+ * @param videoType need to check whether we are screen sharing
|
|
105
|
+ * @param isMuted is video muted
|
|
106
|
+ * @param resolution the input resolution used by the camera
|
|
107
|
+ * @returns {*} the newly calculated value or undefined if no result
|
|
108
|
+ * @private
|
|
109
|
+ */
|
|
110
|
+ _getNewQualityValue(
|
|
111
|
+ data, lastQualityValue, videoType, isMuted, resolution) {
|
|
112
|
+ if (this.disableQualityBasedOnBandwidth
|
|
113
|
+ || isMuted
|
|
114
|
+ || videoType === 'desktop'
|
|
115
|
+ || !resolution) {
|
|
116
|
+ return calculateQuality(
|
|
117
|
+ 100 - data.packetLoss.total,
|
|
118
|
+ lastQualityValue || 100);
|
|
119
|
+ } else {
|
|
120
|
+ return calculateQualityUsingUpload(
|
|
121
|
+ resolution,
|
|
122
|
+ data.bitrate.upload);
|
|
123
|
+ }
|
|
124
|
+ }
|
|
125
|
+
|
|
126
|
+ /**
|
|
127
|
+ * Updates only the localConnectionQuality value
|
|
128
|
+ * @param values {int} the new value. should be from 0 - 100.
|
|
129
|
+ */
|
|
130
|
+ _updateLocalConnectionQuality(value) {
|
|
131
|
+ this.localConnectionQuality = value;
|
|
132
|
+ this.eventEmitter.emit(
|
|
133
|
+ ConnectionQualityEvents.LOCAL_STATS_UPDATED,
|
|
134
|
+ this.localConnectionQuality,
|
|
135
|
+ this.localStats);
|
|
136
|
+ }
|
|
137
|
+
|
|
138
|
+ /**
|
|
139
|
+ * Updates the local statistics
|
|
140
|
+ * @param data new statistics
|
|
141
|
+ * @param dontUpdateLocalConnectionQuality {boolean} if true -
|
|
142
|
+ * localConnectionQuality wont be recalculated.
|
|
143
|
+ * @param videoType the local video type
|
|
144
|
+ * @param isMuted current state of local video, whether it is muted
|
|
145
|
+ * @param resolution the current resolution used by local video
|
|
146
|
+ */
|
|
147
|
+ updateLocalStats(data, dontUpdateLocalConnectionQuality,
|
|
148
|
+ videoType, isMuted, resolution) {
|
|
149
|
+ this.localStats = data;
|
|
150
|
+ if(!dontUpdateLocalConnectionQuality) {
|
|
151
|
+ let val = this._getNewQualityValue(
|
|
152
|
+ this.localStats,
|
|
153
|
+ this.localConnectionQuality,
|
|
154
|
+ videoType,
|
|
155
|
+ isMuted,
|
|
156
|
+ resolution);
|
|
157
|
+ if (val !== undefined)
|
|
158
|
+ this.localConnectionQuality = val;
|
|
159
|
+ }
|
|
160
|
+ this.eventEmitter.emit(
|
|
161
|
+ ConnectionQualityEvents.LOCAL_STATS_UPDATED,
|
|
162
|
+ this.localConnectionQuality,
|
|
163
|
+ this.localStats);
|
|
164
|
+ }
|
|
165
|
+
|
|
166
|
+ /**
|
|
167
|
+ * Updates remote statistics
|
|
168
|
+ * @param id the id associated with the statistics
|
|
169
|
+ * @param data the statistics received
|
|
170
|
+ * @param remoteVideoType the video type of the remote video
|
|
171
|
+ * @param isRemoteVideoMuted whether remote video is muted
|
|
172
|
+ */
|
|
173
|
+ updateRemoteStats(id, data, remoteVideoType, isRemoteVideoMuted) {
|
|
174
|
+ if (!data ||
|
|
175
|
+ !("packetLoss" in data) ||
|
|
176
|
+ !("total" in data.packetLoss)) {
|
|
177
|
+ this.eventEmitter.emit(
|
|
178
|
+ ConnectionQualityEvents.REMOTE_STATS_UPDATED,
|
|
179
|
+ id,
|
|
180
|
+ null,
|
|
181
|
+ null);
|
|
182
|
+ return;
|
|
183
|
+ }
|
|
184
|
+
|
|
185
|
+ let inputResolution = data.resolution;
|
|
186
|
+ // Use only the fields we need
|
|
187
|
+ data = {bitrate: data.bitrate, packetLoss: data.packetLoss};
|
|
188
|
+
|
|
189
|
+ this.remoteStats[id] = data;
|
|
190
|
+
|
|
191
|
+ let val = this._getNewQualityValue(
|
|
192
|
+ data,
|
|
193
|
+ this.remoteConnectionQuality[id],
|
|
194
|
+ remoteVideoType,
|
|
195
|
+ isRemoteVideoMuted,
|
|
196
|
+ inputResolution);
|
|
197
|
+ if (val !== undefined)
|
|
198
|
+ this.remoteConnectionQuality[id] = val;
|
|
199
|
+
|
|
200
|
+ this.eventEmitter.emit(
|
|
201
|
+ ConnectionQualityEvents.REMOTE_STATS_UPDATED,
|
|
202
|
+ id,
|
|
203
|
+ this.remoteConnectionQuality[id],
|
|
204
|
+ this.remoteStats[id]);
|
|
205
|
+ }
|
|
206
|
+
|
|
207
|
+ /**
|
|
208
|
+ * Returns the local statistics.
|
|
209
|
+ */
|
|
210
|
+ getStats() {
|
|
211
|
+ return this.localStats;
|
|
212
|
+ }
|
|
213
|
+}
|