Sfoglia il codice sorgente

Adds RTP stats processing.

j8
paweldomas 11 anni fa
parent
commit
9bfa79ae82
6 ha cambiato i file con 346 aggiunte e 4 eliminazioni
  1. 49
    0
      app.js
  2. 2
    1
      config.js
  3. 1
    0
      index.html
  4. 4
    0
      libs/colibri/colibri.focus.js
  5. 3
    3
      libs/strophe/strophe.jingle.adapter.js
  6. 287
    0
      rtp_stats.js

+ 49
- 0
app.js Vedi File

@@ -8,6 +8,11 @@ var nickname = null;
8 8
 var sharedKey = '';
9 9
 var roomUrl = null;
10 10
 var ssrc2jid = {};
11
+/**
12
+ * The stats collector that process stats data and triggers updates to app.js.
13
+ * @type {StatsCollector}
14
+ */
15
+var statsCollector = null;
11 16
 
12 17
 /**
13 18
  * Indicates whether ssrc is camera video or desktop stream.
@@ -464,12 +469,46 @@ function muteVideo(pc, unmute) {
464 469
     );
465 470
 }
466 471
 
472
+/**
473
+ * Callback called by {@link StatsCollector} in intervals supplied to it's
474
+ * constructor.
475
+ * @param statsCollector {@link StatsCollector} source of the event.
476
+ */
477
+function statsUpdated(statsCollector)
478
+{
479
+    Object.keys(statsCollector.jid2stats).forEach(function (jid)
480
+    {
481
+        var peerStats = statsCollector.jid2stats[jid];
482
+        Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc)
483
+        {
484
+            console.info(jid +  " audio level: " +
485
+                peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc);
486
+        });
487
+    });
488
+}
489
+
490
+/**
491
+ * Starts the {@link StatsCollector} if the feature is enabled in config.js.
492
+ */
493
+function startRtpStatsCollector()
494
+{
495
+    if (config.enableRtpStats)
496
+    {
497
+        statsCollector = new StatsCollector(
498
+            getConferenceHandler().peerconnection, 200, statsUpdated);
499
+
500
+        statsCollector.start();
501
+    }
502
+}
503
+
467 504
 $(document).bind('callincoming.jingle', function (event, sid) {
468 505
     var sess = connection.jingle.sessions[sid];
469 506
 
470 507
     // TODO: do we check activecall == null?
471 508
     activecall = sess;
472 509
 
510
+    startRtpStatsCollector();
511
+
473 512
     // TODO: check affiliation and/or role
474 513
     console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
475 514
     sess.usedrip = true; // not-so-naive trickle ice
@@ -478,6 +517,11 @@ $(document).bind('callincoming.jingle', function (event, sid) {
478 517
 
479 518
 });
480 519
 
520
+$(document).bind('conferenceCreated.jingle', function (event, focus)
521
+{
522
+    startRtpStatsCollector();
523
+});
524
+
481 525
 $(document).bind('callactive.jingle', function (event, videoelem, sid) {
482 526
     if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
483 527
         // ignore mixedmslabela0 and v0
@@ -1165,6 +1209,11 @@ function disposeConference() {
1165 1209
         }
1166 1210
         handler.peerconnection.close();
1167 1211
     }
1212
+    if (statsCollector)
1213
+    {
1214
+        statsCollector.stop();
1215
+        statsCollector = null;
1216
+    }
1168 1217
     focus = null;
1169 1218
     activecall = null;
1170 1219
 }

+ 2
- 1
config.js Vedi File

@@ -11,5 +11,6 @@ var config = {
11 11
     bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
12 12
     desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
13 13
     chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
14
-    minChromeExtVersion: '0.1' // Required version of Chrome extension
14
+    minChromeExtVersion: '0.1', // Required version of Chrome extension
15
+    enableRtpStats: false // Enables RTP stats processing
15 16
 };

+ 1
- 0
index.html Vedi File

@@ -33,6 +33,7 @@
33 33
     <script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
34 34
     <script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
35 35
     <script src="analytics.js?v=1"></script><!-- google analytics plugin -->
36
+    <script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
36 37
     <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
37 38
     <link rel="stylesheet" href="css/font.css"/>
38 39
     <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=20"/>

+ 4
- 0
libs/colibri/colibri.focus.js Vedi File

@@ -344,6 +344,10 @@ ColibriFocus.prototype.createdConference = function (result) {
344 344
                             for (var i = 0; i < numparticipants; i++) {
345 345
                                 self.initiate(self.peers[i], true);
346 346
                             }
347
+
348
+                            // Notify we've created the conference
349
+                            $(document).trigger(
350
+                                'conferenceCreated.jingle', self);
347 351
                         },
348 352
                         function (error) {
349 353
                             console.warn('setLocalDescription failed.', error);

+ 3
- 3
libs/strophe/strophe.jingle.adapter.js Vedi File

@@ -5,7 +5,7 @@ function TraceablePeerConnection(ice_config, constraints) {
5 5
     this.updateLog = [];
6 6
     this.stats = {};
7 7
     this.statsinterval = null;
8
-    this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
8
+    this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
9 9
 
10 10
     /**
11 11
      * Array of ssrcs that will be added on next modifySources call.
@@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
88 88
         if (self.ondatachannel !== null) {
89 89
             self.ondatachannel(event);
90 90
         }
91
-    }
92
-    if (!navigator.mozGetUserMedia) {
91
+    };
92
+    if (!navigator.mozGetUserMedia && this.maxstats) {
93 93
         this.statsinterval = window.setInterval(function() {
94 94
             self.peerconnection.getStats(function(stats) {
95 95
                 var results = stats.result();

+ 287
- 0
rtp_stats.js Vedi File

@@ -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
+

Loading…
Annulla
Salva