123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- /* global ssrc2jid */
-
- /**
- * Function object which once created can be used to calculate moving average of
- * given period. Example for SMA3:</br>
- * var sma3 = new SimpleMovingAverager(3);
- * while(true) // some update loop
- * {
- * var currentSma3Value = sma3(nextInputValue);
- * }
- *
- * @param period moving average period that will be used by created instance.
- * @returns {Function} SMA calculator function of given <tt>period</tt>.
- * @constructor
- */
- function SimpleMovingAverager(period)
- {
- var nums = [];
- return function (num)
- {
- nums.push(num);
- if (nums.length > period)
- nums.splice(0, 1);
- var sum = 0;
- for (var i in nums)
- sum += nums[i];
- var n = period;
- if (nums.length < period)
- n = nums.length;
- return (sum / n);
- };
- }
-
- /**
- * Peer statistics data holder.
- * @constructor
- */
- function PeerStats()
- {
- this.ssrc2Loss = {};
- this.ssrc2AudioLevel = {};
- }
-
- /**
- * Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
- * represented by this instance.
- * @param ssrc audio or video RTP stream SSRC.
- * @param lossRate new packet loss rate value to be set.
- */
- PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
- {
- this.ssrc2Loss[ssrc] = lossRate;
- };
-
- /**
- * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
- * the stream which belongs to the peer represented by this instance.
- * @param ssrc RTP stream SSRC for which current audio level value will be
- * updated.
- * @param audioLevel the new audio level value to be set. Value is truncated to
- * fit the range from 0 to 1.
- */
- PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
- {
- // Range limit 0 - 1
- this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
- };
-
- /**
- * Calculates average packet loss for all streams that belong to the peer
- * represented by this instance.
- * @returns {number} average packet loss for all streams that belong to the peer
- * represented by this instance.
- */
- PeerStats.prototype.getAvgLoss = function ()
- {
- var self = this;
- var avg = 0;
- var count = Object.keys(this.ssrc2Loss).length;
- Object.keys(this.ssrc2Loss).forEach(
- function (ssrc)
- {
- avg += self.ssrc2Loss[ssrc];
- }
- );
- return count > 0 ? avg / count : 0;
- };
-
- /**
- * <tt>StatsCollector</tt> registers for stats updates of given
- * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
- * stats are extracted and put in {@link PeerStats} objects. Once the processing
- * is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
- * an event source.
- *
- * @param peerconnection webRTC peer connection object.
- * @param interval stats refresh interval given in ms.
- * @param {function(StatsCollector)} updateCallback the callback called on stats
- * update.
- * @constructor
- */
- function StatsCollector(peerconnection, interval, updateCallback)
- {
- this.peerconnection = peerconnection;
- this.baselineReport = null;
- this.currentReport = null;
- this.intervalId = null;
- // Updates stats interval
- this.intervalMilis = interval;
- // Use SMA 3 to average packet loss changes over time
- this.sma3 = new SimpleMovingAverager(3);
- // Map of jids to PeerStats
- this.jid2stats = {};
-
- this.updateCallback = updateCallback;
- }
-
- /**
- * Stops stats updates.
- */
- StatsCollector.prototype.stop = function ()
- {
- if (this.intervalId)
- {
- clearInterval(this.intervalId);
- this.intervalId = null;
- }
- };
-
- /**
- * Callback passed to <tt>getStats</tt> method.
- * @param error an error that occurred on <tt>getStats</tt> call.
- */
- StatsCollector.prototype.errorCallback = function (error)
- {
- console.error("Get stats error", error);
- this.stop();
- };
-
- /**
- * Starts stats updates.
- */
- StatsCollector.prototype.start = function ()
- {
- var self = this;
- this.intervalId = setInterval(
- function ()
- {
- // Interval updates
- self.peerconnection.getStats(
- function (report)
- {
- var results = report.result();
- //console.error("Got interval report", results);
- self.currentReport = results;
- self.processReport();
- self.baselineReport = self.currentReport;
- },
- self.errorCallback
- );
- },
- self.intervalMilis
- );
- };
-
- /**
- * Stats processing logic.
- */
- StatsCollector.prototype.processReport = function ()
- {
- if (!this.baselineReport)
- {
- return;
- }
-
- for (var idx in this.currentReport)
- {
- var now = this.currentReport[idx];
- if (now.type != 'ssrc')
- {
- continue;
- }
-
- var before = this.baselineReport[idx];
- if (!before)
- {
- console.warn(now.stat('ssrc') + ' not enough data');
- continue;
- }
-
- var ssrc = now.stat('ssrc');
- var jid = ssrc2jid[ssrc];
- if (!jid)
- {
- console.warn("No jid for ssrc: " + ssrc);
- continue;
- }
-
- var jidStats = this.jid2stats[jid];
- if (!jidStats)
- {
- jidStats = new PeerStats();
- this.jid2stats[jid] = jidStats;
- }
-
- // Audio level
- var audioLevel = now.stat('audioInputLevel');
- if (!audioLevel)
- audioLevel = now.stat('audioOutputLevel');
- if (audioLevel)
- {
- // TODO: can't find specs about what this value really is,
- // but it seems to vary between 0 and around 32k.
- audioLevel = audioLevel / 32767;
- jidStats.setSsrcAudioLevel(ssrc, audioLevel);
- if(jid != connection.emuc.myroomjid)
- this.updateCallback(jid, audioLevel);
- }
-
- var key = 'packetsReceived';
- if (!now.stat(key))
- {
- key = 'packetsSent';
- if (!now.stat(key))
- {
- console.error("No packetsReceived nor packetSent stat found");
- this.stop();
- return;
- }
- }
- var packetsNow = now.stat(key);
- var packetsBefore = before.stat(key);
- var packetRate = packetsNow - packetsBefore;
-
- var currentLoss = now.stat('packetsLost');
- var previousLoss = before.stat('packetsLost');
- var lossRate = currentLoss - previousLoss;
-
- var packetsTotal = (packetRate + lossRate);
- var lossPercent;
-
- if (packetsTotal > 0)
- lossPercent = lossRate / packetsTotal;
- else
- lossPercent = 0;
-
- //console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow);
-
- jidStats.setSsrcLoss(ssrc, lossPercent);
- }
-
- var self = this;
- // Jid stats
- var allPeersAvg = 0;
- var jids = Object.keys(this.jid2stats);
- jids.forEach(
- function (jid)
- {
- var peerAvg = self.jid2stats[jid].getAvgLoss(
- function (avg)
- {
- //console.info(jid + " stats: " + (avg * 100) + " %");
- allPeersAvg += avg;
- }
- );
- }
- );
-
- if (jids.length > 1)
- {
- // Our streams loss is reported as 0 always, so -1 to length
- allPeersAvg = allPeersAvg / (jids.length - 1);
-
- /**
- * Calculates number of connection quality bars from 4(hi) to 0(lo).
- */
- var outputAvg = self.sma3(allPeersAvg);
- // Linear from 4(0%) to 0(25%).
- var quality = Math.round(4 - outputAvg * 16);
- quality = Math.max(quality, 0); // lower limit 0
- quality = Math.min(quality, 4); // upper limit 4
- // TODO: quality can be used to indicate connection quality using 4 step
- // bar indicator
- //console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
- }
- };
|