Adds RTP stats processing.
This commit is contained in:
parent
96b7a3d3e1
commit
9bfa79ae82
49
app.js
49
app.js
|
@ -8,6 +8,11 @@ var nickname = null;
|
||||||
var sharedKey = '';
|
var sharedKey = '';
|
||||||
var roomUrl = null;
|
var roomUrl = null;
|
||||||
var ssrc2jid = {};
|
var ssrc2jid = {};
|
||||||
|
/**
|
||||||
|
* The stats collector that process stats data and triggers updates to app.js.
|
||||||
|
* @type {StatsCollector}
|
||||||
|
*/
|
||||||
|
var statsCollector = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether ssrc is camera video or desktop stream.
|
* Indicates whether ssrc is camera video or desktop stream.
|
||||||
|
@ -464,12 +469,46 @@ function muteVideo(pc, unmute) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback called by {@link StatsCollector} in intervals supplied to it's
|
||||||
|
* constructor.
|
||||||
|
* @param statsCollector {@link StatsCollector} source of the event.
|
||||||
|
*/
|
||||||
|
function statsUpdated(statsCollector)
|
||||||
|
{
|
||||||
|
Object.keys(statsCollector.jid2stats).forEach(function (jid)
|
||||||
|
{
|
||||||
|
var peerStats = statsCollector.jid2stats[jid];
|
||||||
|
Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc)
|
||||||
|
{
|
||||||
|
console.info(jid + " audio level: " +
|
||||||
|
peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the {@link StatsCollector} if the feature is enabled in config.js.
|
||||||
|
*/
|
||||||
|
function startRtpStatsCollector()
|
||||||
|
{
|
||||||
|
if (config.enableRtpStats)
|
||||||
|
{
|
||||||
|
statsCollector = new StatsCollector(
|
||||||
|
getConferenceHandler().peerconnection, 200, statsUpdated);
|
||||||
|
|
||||||
|
statsCollector.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(document).bind('callincoming.jingle', function (event, sid) {
|
$(document).bind('callincoming.jingle', function (event, sid) {
|
||||||
var sess = connection.jingle.sessions[sid];
|
var sess = connection.jingle.sessions[sid];
|
||||||
|
|
||||||
// TODO: do we check activecall == null?
|
// TODO: do we check activecall == null?
|
||||||
activecall = sess;
|
activecall = sess;
|
||||||
|
|
||||||
|
startRtpStatsCollector();
|
||||||
|
|
||||||
// TODO: check affiliation and/or role
|
// TODO: check affiliation and/or role
|
||||||
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
|
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
|
||||||
sess.usedrip = true; // not-so-naive trickle ice
|
sess.usedrip = true; // not-so-naive trickle ice
|
||||||
|
@ -478,6 +517,11 @@ $(document).bind('callincoming.jingle', function (event, sid) {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).bind('conferenceCreated.jingle', function (event, focus)
|
||||||
|
{
|
||||||
|
startRtpStatsCollector();
|
||||||
|
});
|
||||||
|
|
||||||
$(document).bind('callactive.jingle', function (event, videoelem, sid) {
|
$(document).bind('callactive.jingle', function (event, videoelem, sid) {
|
||||||
if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
|
if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
|
||||||
// ignore mixedmslabela0 and v0
|
// ignore mixedmslabela0 and v0
|
||||||
|
@ -1165,6 +1209,11 @@ function disposeConference() {
|
||||||
}
|
}
|
||||||
handler.peerconnection.close();
|
handler.peerconnection.close();
|
||||||
}
|
}
|
||||||
|
if (statsCollector)
|
||||||
|
{
|
||||||
|
statsCollector.stop();
|
||||||
|
statsCollector = null;
|
||||||
|
}
|
||||||
focus = null;
|
focus = null;
|
||||||
activecall = null;
|
activecall = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,6 @@ var config = {
|
||||||
bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
|
bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
|
||||||
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
|
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
|
||||||
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
|
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
|
||||||
minChromeExtVersion: '0.1' // Required version of Chrome extension
|
minChromeExtVersion: '0.1', // Required version of Chrome extension
|
||||||
|
enableRtpStats: false // Enables RTP stats processing
|
||||||
};
|
};
|
|
@ -33,6 +33,7 @@
|
||||||
<script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
|
<script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
|
||||||
<script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
|
<script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
|
||||||
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
||||||
|
<script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
|
||||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/font.css"/>
|
<link rel="stylesheet" href="css/font.css"/>
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=20"/>
|
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=20"/>
|
||||||
|
|
|
@ -344,6 +344,10 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
for (var i = 0; i < numparticipants; i++) {
|
for (var i = 0; i < numparticipants; i++) {
|
||||||
self.initiate(self.peers[i], true);
|
self.initiate(self.peers[i], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify we've created the conference
|
||||||
|
$(document).trigger(
|
||||||
|
'conferenceCreated.jingle', self);
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
console.warn('setLocalDescription failed.', error);
|
console.warn('setLocalDescription failed.', error);
|
||||||
|
|
|
@ -5,7 +5,7 @@ function TraceablePeerConnection(ice_config, constraints) {
|
||||||
this.updateLog = [];
|
this.updateLog = [];
|
||||||
this.stats = {};
|
this.stats = {};
|
||||||
this.statsinterval = null;
|
this.statsinterval = null;
|
||||||
this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of ssrcs that will be added on next modifySources call.
|
* Array of ssrcs that will be added on next modifySources call.
|
||||||
|
@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
|
||||||
if (self.ondatachannel !== null) {
|
if (self.ondatachannel !== null) {
|
||||||
self.ondatachannel(event);
|
self.ondatachannel(event);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
if (!navigator.mozGetUserMedia) {
|
if (!navigator.mozGetUserMedia && this.maxstats) {
|
||||||
this.statsinterval = window.setInterval(function() {
|
this.statsinterval = window.setInterval(function() {
|
||||||
self.peerconnection.getStats(function(stats) {
|
self.peerconnection.getStats(function(stats) {
|
||||||
var results = stats.result();
|
var results = stats.result();
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue