|
@@ -0,0 +1,287 @@
|
|
1
|
+/* global ssrc2jid */
|
|
2
|
+
|
|
3
|
+/**
|
|
4
|
+ * Function object which once created can be used to calculate moving average of
|
|
5
|
+ * given period. Example for SMA3:</br>
|
|
6
|
+ * var sma3 = new SimpleMovingAverager(3);
|
|
7
|
+ * while(true) // some update loop
|
|
8
|
+ * {
|
|
9
|
+ * var currentSma3Value = sma3(nextInputValue);
|
|
10
|
+ * }
|
|
11
|
+ *
|
|
12
|
+ * @param period moving average period that will be used by created instance.
|
|
13
|
+ * @returns {Function} SMA calculator function of given <tt>period</tt>.
|
|
14
|
+ * @constructor
|
|
15
|
+ */
|
|
16
|
+function SimpleMovingAverager(period)
|
|
17
|
+{
|
|
18
|
+ var nums = [];
|
|
19
|
+ return function (num)
|
|
20
|
+ {
|
|
21
|
+ nums.push(num);
|
|
22
|
+ if (nums.length > period)
|
|
23
|
+ nums.splice(0, 1);
|
|
24
|
+ var sum = 0;
|
|
25
|
+ for (var i in nums)
|
|
26
|
+ sum += nums[i];
|
|
27
|
+ var n = period;
|
|
28
|
+ if (nums.length < period)
|
|
29
|
+ n = nums.length;
|
|
30
|
+ return (sum / n);
|
|
31
|
+ };
|
|
32
|
+}
|
|
33
|
+
|
|
34
|
+/**
|
|
35
|
+ * Peer statistics data holder.
|
|
36
|
+ * @constructor
|
|
37
|
+ */
|
|
38
|
+function PeerStats()
|
|
39
|
+{
|
|
40
|
+ this.ssrc2Loss = {};
|
|
41
|
+ this.ssrc2AudioLevel = {};
|
|
42
|
+}
|
|
43
|
+
|
|
44
|
+/**
|
|
45
|
+ * Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
|
|
46
|
+ * represented by this instance.
|
|
47
|
+ * @param ssrc audio or video RTP stream SSRC.
|
|
48
|
+ * @param lossRate new packet loss rate value to be set.
|
|
49
|
+ */
|
|
50
|
+PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
|
|
51
|
+{
|
|
52
|
+ this.ssrc2Loss[ssrc] = lossRate;
|
|
53
|
+};
|
|
54
|
+
|
|
55
|
+/**
|
|
56
|
+ * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
|
|
57
|
+ * the stream which belongs to the peer represented by this instance.
|
|
58
|
+ * @param ssrc RTP stream SSRC for which current audio level value will be
|
|
59
|
+ * updated.
|
|
60
|
+ * @param audioLevel the new audio level value to be set. Value is truncated to
|
|
61
|
+ * fit the range from 0 to 1.
|
|
62
|
+ */
|
|
63
|
+PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
|
|
64
|
+{
|
|
65
|
+ // Range limit 0 - 1
|
|
66
|
+ this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
|
|
67
|
+};
|
|
68
|
+
|
|
69
|
+/**
|
|
70
|
+ * Calculates average packet loss for all streams that belong to the peer
|
|
71
|
+ * represented by this instance.
|
|
72
|
+ * @returns {number} average packet loss for all streams that belong to the peer
|
|
73
|
+ * represented by this instance.
|
|
74
|
+ */
|
|
75
|
+PeerStats.prototype.getAvgLoss = function ()
|
|
76
|
+{
|
|
77
|
+ var self = this;
|
|
78
|
+ var avg = 0;
|
|
79
|
+ var count = Object.keys(this.ssrc2Loss).length;
|
|
80
|
+ Object.keys(this.ssrc2Loss).forEach(
|
|
81
|
+ function (ssrc)
|
|
82
|
+ {
|
|
83
|
+ avg += self.ssrc2Loss[ssrc];
|
|
84
|
+ }
|
|
85
|
+ );
|
|
86
|
+ return count > 0 ? avg / count : 0;
|
|
87
|
+};
|
|
88
|
+
|
|
89
|
+/**
|
|
90
|
+ * <tt>StatsCollector</tt> registers for stats updates of given
|
|
91
|
+ * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
|
|
92
|
+ * stats are extracted and put in {@link PeerStats} objects. Once the processing
|
|
93
|
+ * is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
|
|
94
|
+ * an event source.
|
|
95
|
+ *
|
|
96
|
+ * @param peerconnection webRTC peer connection object.
|
|
97
|
+ * @param interval stats refresh interval given in ms.
|
|
98
|
+ * @param {function(StatsCollector)} updateCallback the callback called on stats
|
|
99
|
+ * update.
|
|
100
|
+ * @constructor
|
|
101
|
+ */
|
|
102
|
+function StatsCollector(peerconnection, interval, updateCallback)
|
|
103
|
+{
|
|
104
|
+ this.peerconnection = peerconnection;
|
|
105
|
+ this.baselineReport = null;
|
|
106
|
+ this.currentReport = null;
|
|
107
|
+ this.intervalId = null;
|
|
108
|
+ // Updates stats interval
|
|
109
|
+ this.intervalMilis = interval;
|
|
110
|
+ // Use SMA 3 to average packet loss changes over time
|
|
111
|
+ this.sma3 = new SimpleMovingAverager(3);
|
|
112
|
+ // Map of jids to PeerStats
|
|
113
|
+ this.jid2stats = {};
|
|
114
|
+
|
|
115
|
+ this.updateCallback = updateCallback;
|
|
116
|
+}
|
|
117
|
+
|
|
118
|
+/**
|
|
119
|
+ * Stops stats updates.
|
|
120
|
+ */
|
|
121
|
+StatsCollector.prototype.stop = function ()
|
|
122
|
+{
|
|
123
|
+ if (this.intervalId)
|
|
124
|
+ {
|
|
125
|
+ clearInterval(this.intervalId);
|
|
126
|
+ this.intervalId = null;
|
|
127
|
+ }
|
|
128
|
+};
|
|
129
|
+
|
|
130
|
+/**
|
|
131
|
+ * Callback passed to <tt>getStats</tt> method.
|
|
132
|
+ * @param error an error that occurred on <tt>getStats</tt> call.
|
|
133
|
+ */
|
|
134
|
+StatsCollector.prototype.errorCallback = function (error)
|
|
135
|
+{
|
|
136
|
+ console.error("Get stats error", error);
|
|
137
|
+ this.stop();
|
|
138
|
+};
|
|
139
|
+
|
|
140
|
+/**
|
|
141
|
+ * Starts stats updates.
|
|
142
|
+ */
|
|
143
|
+StatsCollector.prototype.start = function ()
|
|
144
|
+{
|
|
145
|
+ var self = this;
|
|
146
|
+ this.intervalId = setInterval(
|
|
147
|
+ function ()
|
|
148
|
+ {
|
|
149
|
+ // Interval updates
|
|
150
|
+ self.peerconnection.getStats(
|
|
151
|
+ function (report)
|
|
152
|
+ {
|
|
153
|
+ var results = report.result();
|
|
154
|
+ //console.error("Got interval report", results);
|
|
155
|
+ self.currentReport = results;
|
|
156
|
+ self.processReport();
|
|
157
|
+ self.baselineReport = self.currentReport;
|
|
158
|
+ },
|
|
159
|
+ self.errorCallback
|
|
160
|
+ );
|
|
161
|
+ },
|
|
162
|
+ self.intervalMilis
|
|
163
|
+ );
|
|
164
|
+};
|
|
165
|
+
|
|
166
|
+/**
|
|
167
|
+ * Stats processing logic.
|
|
168
|
+ */
|
|
169
|
+StatsCollector.prototype.processReport = function ()
|
|
170
|
+{
|
|
171
|
+ if (!this.baselineReport)
|
|
172
|
+ {
|
|
173
|
+ return;
|
|
174
|
+ }
|
|
175
|
+
|
|
176
|
+ for (var idx in this.currentReport)
|
|
177
|
+ {
|
|
178
|
+ var now = this.currentReport[idx];
|
|
179
|
+ if (now.type != 'ssrc')
|
|
180
|
+ {
|
|
181
|
+ continue;
|
|
182
|
+ }
|
|
183
|
+
|
|
184
|
+ var before = this.baselineReport[idx];
|
|
185
|
+ if (!before)
|
|
186
|
+ {
|
|
187
|
+ console.warn(now.stat('ssrc') + ' not enough data');
|
|
188
|
+ continue;
|
|
189
|
+ }
|
|
190
|
+
|
|
191
|
+ var ssrc = now.stat('ssrc');
|
|
192
|
+ var jid = ssrc2jid[ssrc];
|
|
193
|
+ if (!jid)
|
|
194
|
+ {
|
|
195
|
+ console.warn("No jid for ssrc: " + ssrc);
|
|
196
|
+ continue;
|
|
197
|
+ }
|
|
198
|
+
|
|
199
|
+ var jidStats = this.jid2stats[jid];
|
|
200
|
+ if (!jidStats)
|
|
201
|
+ {
|
|
202
|
+ jidStats = new PeerStats();
|
|
203
|
+ this.jid2stats[jid] = jidStats;
|
|
204
|
+ }
|
|
205
|
+
|
|
206
|
+ // Audio level
|
|
207
|
+ var audioLevel = now.stat('audioInputLevel');
|
|
208
|
+ if (!audioLevel)
|
|
209
|
+ audioLevel = now.stat('audioOutputLevel');
|
|
210
|
+ if (audioLevel)
|
|
211
|
+ {
|
|
212
|
+ // TODO: can't find specs about what this value really is,
|
|
213
|
+ // but it seems to vary between 0 and around 32k.
|
|
214
|
+ audioLevel = audioLevel / 32767;
|
|
215
|
+ jidStats.setSsrcAudioLevel(ssrc, audioLevel);
|
|
216
|
+ }
|
|
217
|
+
|
|
218
|
+ var key = 'packetsReceived';
|
|
219
|
+ if (!now.stat(key))
|
|
220
|
+ {
|
|
221
|
+ key = 'packetsSent';
|
|
222
|
+ if (!now.stat(key))
|
|
223
|
+ {
|
|
224
|
+ console.error("No packetsReceived nor packetSent stat found");
|
|
225
|
+ this.stop();
|
|
226
|
+ return;
|
|
227
|
+ }
|
|
228
|
+ }
|
|
229
|
+ var packetsNow = now.stat(key);
|
|
230
|
+ var packetsBefore = before.stat(key);
|
|
231
|
+ var packetRate = packetsNow - packetsBefore;
|
|
232
|
+
|
|
233
|
+ var currentLoss = now.stat('packetsLost');
|
|
234
|
+ var previousLoss = before.stat('packetsLost');
|
|
235
|
+ var lossRate = currentLoss - previousLoss;
|
|
236
|
+
|
|
237
|
+ var packetsTotal = (packetRate + lossRate);
|
|
238
|
+ var lossPercent;
|
|
239
|
+
|
|
240
|
+ if (packetsTotal > 0)
|
|
241
|
+ lossPercent = lossRate / packetsTotal;
|
|
242
|
+ else
|
|
243
|
+ lossPercent = 0;
|
|
244
|
+
|
|
245
|
+ //console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow);
|
|
246
|
+
|
|
247
|
+ jidStats.setSsrcLoss(ssrc, lossPercent);
|
|
248
|
+ }
|
|
249
|
+
|
|
250
|
+ var self = this;
|
|
251
|
+ // Jid stats
|
|
252
|
+ var allPeersAvg = 0;
|
|
253
|
+ var jids = Object.keys(this.jid2stats);
|
|
254
|
+ jids.forEach(
|
|
255
|
+ function (jid)
|
|
256
|
+ {
|
|
257
|
+ var peerAvg = self.jid2stats[jid].getAvgLoss(
|
|
258
|
+ function (avg)
|
|
259
|
+ {
|
|
260
|
+ //console.info(jid + " stats: " + (avg * 100) + " %");
|
|
261
|
+ allPeersAvg += avg;
|
|
262
|
+ }
|
|
263
|
+ );
|
|
264
|
+ }
|
|
265
|
+ );
|
|
266
|
+
|
|
267
|
+ if (jids.length > 1)
|
|
268
|
+ {
|
|
269
|
+ // Our streams loss is reported as 0 always, so -1 to length
|
|
270
|
+ allPeersAvg = allPeersAvg / (jids.length - 1);
|
|
271
|
+
|
|
272
|
+ /**
|
|
273
|
+ * Calculates number of connection quality bars from 4(hi) to 0(lo).
|
|
274
|
+ */
|
|
275
|
+ var outputAvg = self.sma3(allPeersAvg);
|
|
276
|
+ // Linear from 4(0%) to 0(25%).
|
|
277
|
+ var quality = Math.round(4 - outputAvg * 16);
|
|
278
|
+ quality = Math.max(quality, 0); // lower limit 0
|
|
279
|
+ quality = Math.min(quality, 4); // upper limit 4
|
|
280
|
+ // TODO: quality can be used to indicate connection quality using 4 step
|
|
281
|
+ // bar indicator
|
|
282
|
+ //console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
|
|
283
|
+ }
|
|
284
|
+
|
|
285
|
+ self.updateCallback(self);
|
|
286
|
+};
|
|
287
|
+
|