/* global ssrc2jid */ /** * Function object which once created can be used to calculate moving average of * given period. Example for SMA3:
* 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 period. * @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 ssrc 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 ssrc 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; }; /** * StatsCollector registers for stats updates of given * peerconnection in given interval. On each update particular * stats are extracted and put in {@link PeerStats} objects. Once the processing * is done updateCallback is called with this 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 getStats method. * @param error an error that occurred on getStats 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); } 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); } self.updateCallback(self); };