jiti-meet/rtp_sts.js

606 lines
17 KiB
JavaScript
Raw Normal View History

2014-06-05 11:09:31 +00:00
/* global ssrc2jid */
/* jshint -W117 */
2014-06-05 11:09:31 +00:00
/**
* Calculates packet lost percent using the number of lost packets and the
* number of all packet.
* @param lostPackets the number of lost packets
* @param totalPackets the number of all packets.
* @returns {number} packet loss percent
2014-06-05 11:09:31 +00:00
*/
function calculatePacketLoss(lostPackets, totalPackets) {
if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0)
return 0;
return Math.round((lostPackets/totalPackets)*100);
2014-06-05 11:09:31 +00:00
}
/**
* Peer statistics data holder.
* @constructor
*/
function PeerStats()
{
this.ssrc2Loss = {};
this.ssrc2AudioLevel = {};
this.ssrc2bitrate = {};
this.ssrc2resolution = {};
2014-06-05 11:09:31 +00:00
}
/**
* The bandwidth
* @type {{}}
*/
PeerStats.bandwidth = {};
/**
* The bit rate
* @type {{}}
*/
PeerStats.bitrate = {};
/**
* The packet loss rate
* @type {{}}
*/
PeerStats.packetLoss = null;
2014-06-05 11:09:31 +00:00
/**
* 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 resolution for given <tt>ssrc</tt> that belong to the peer
* represented by this instance.
* @param ssrc audio or video RTP stream SSRC.
* @param resolution new resolution value to be set.
*/
PeerStats.prototype.setSsrcResolution = function (ssrc, resolution)
{
if(resolution === null && this.ssrc2resolution[ssrc])
{
delete this.ssrc2resolution[ssrc];
}
else if(resolution !== null)
this.ssrc2resolution[ssrc] = resolution;
};
/**
* Sets the bit rate for given <tt>ssrc</tt> that blong to the peer
* represented by this instance.
* @param ssrc audio or video RTP stream SSRC.
* @param bitrate new bitrate value to be set.
*/
PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate)
{
this.ssrc2bitrate[ssrc] = bitrate;
};
2014-06-05 11:09:31 +00:00
/**
* 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);
};
/**
* Array with the transport information.
* @type {Array}
2014-06-05 11:09:31 +00:00
*/
PeerStats.transport = [];
2014-06-05 11:09:31 +00:00
/**
* <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>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
* instance as an event source.
2014-06-05 11:09:31 +00:00
*
* @param peerconnection webRTC peer connection object.
* @param interval stats refresh interval given in ms.
* @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
* called on stats update.
2014-06-05 11:09:31 +00:00
* @constructor
*/
function StatsCollector(peerconnection, audioLevelsInterval,
audioLevelsUpdateCallback, statsInterval,
statsUpdateCallback)
2014-06-05 11:09:31 +00:00
{
this.peerconnection = peerconnection;
this.baselineAudioLevelsReport = null;
this.currentAudioLevelsReport = null;
this.currentStatsReport = null;
this.baselineStatsReport = null;
this.audioLevelsIntervalId = null;
/**
* Gather PeerConnection stats once every this many milliseconds.
*/
this.GATHER_INTERVAL = 10000;
/**
* Log stats via the focus once every this many milliseconds.
*/
this.LOG_INTERVAL = 60000;
/**
* Gather stats and store them in this.statsToBeLogged.
*/
this.gatherStatsIntervalId = null;
/**
* Send the stats already saved in this.statsToBeLogged to be logged via
* the focus.
*/
this.logStatsIntervalId = null;
/**
* Stores the statistics which will be send to the focus to be logged.
*/
this.statsToBeLogged =
{
timestamps: [],
stats: {}
};
2014-06-05 11:09:31 +00:00
// Updates stats interval
this.audioLevelsIntervalMilis = audioLevelsInterval;
this.statsIntervalId = null;
this.statsIntervalMilis = statsInterval;
2014-06-05 11:09:31 +00:00
// Map of jids to PeerStats
this.jid2stats = {};
this.audioLevelsUpdateCallback = audioLevelsUpdateCallback;
this.statsUpdateCallback = statsUpdateCallback;
2014-06-05 11:09:31 +00:00
}
/**
* Stops stats updates.
*/
StatsCollector.prototype.stop = function ()
{
if (this.audioLevelsIntervalId)
2014-06-05 11:09:31 +00:00
{
clearInterval(this.audioLevelsIntervalId);
this.audioLevelsIntervalId = null;
clearInterval(this.statsIntervalId);
this.statsIntervalId = null;
clearInterval(this.logStatsIntervalId);
this.logStatsIntervalId = null;
clearInterval(this.gatherStatsIntervalId);
this.gatherStatsIntervalId = null;
2014-06-05 11:09:31 +00:00
}
};
/**
* 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.audioLevelsIntervalId = setInterval(
2014-06-05 11:09:31 +00:00
function ()
{
// Interval updates
self.peerconnection.getStats(
function (report)
{
var results = report.result();
//console.error("Got interval report", results);
self.currentAudioLevelsReport = results;
self.processAudioLevelReport();
self.baselineAudioLevelsReport =
self.currentAudioLevelsReport;
},
self.errorCallback
);
},
self.audioLevelsIntervalMilis
);
this.statsIntervalId = setInterval(
function () {
// Interval updates
self.peerconnection.getStats(
function (report)
{
var results = report.result();
//console.error("Got interval report", results);
self.currentStatsReport = results;
self.processStatsReport();
self.baselineStatsReport = self.currentStatsReport;
2014-06-05 11:09:31 +00:00
},
self.errorCallback
);
},
self.statsIntervalMilis
2014-06-05 11:09:31 +00:00
);
if (config.logStats) {
this.gatherStatsIntervalId = setInterval(
function () {
self.peerconnection.getStats(
function (report) {
self.addStatsToBeLogged(report.result());
},
function () {
}
);
},
this.GATHER_INTERVAL
);
this.logStatsIntervalId = setInterval(
function() { self.logStats(); },
this.LOG_INTERVAL);
}
};
/**
* Converts the stats to the format used for logging, and saves the data in
* this.statsToBeLogged.
* @param reports Reports as given by webkitRTCPerConnection.getStats.
*/
StatsCollector.prototype.addStatsToBeLogged = function (reports) {
var self = this;
var num_records = this.statsToBeLogged.timestamps.length;
this.statsToBeLogged.timestamps.push(new Date().getTime());
reports.map(function (report) {
var stat = self.statsToBeLogged.stats[report.id];
if (!stat) {
stat = self.statsToBeLogged.stats[report.id] = {};
}
stat.type = report.type;
report.names().map(function (name) {
var values = stat[name];
if (!values) {
values = stat[name] = [];
}
while (values.length < num_records) {
values.push(null);
}
values.push(report.stat(name));
});
});
2014-06-05 11:09:31 +00:00
};
StatsCollector.prototype.logStats = function () {
if (!focusJid) {
return;
}
var deflate = true;
var content = JSON.stringify(this.statsToBeLogged);
if (deflate) {
content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
}
content = Base64.encode(content);
// XEP-0337-ish
var message = $msg({to: focusJid, type: 'normal'});
message.c('log', { xmlns: 'urn:xmpp:eventlog',
id: 'PeerConnectionStats'});
message.c('message').t(content).up();
if (deflate) {
message.c('tag', {name: "deflated", value: "true"}).up();
}
message.up();
connection.send(message);
// Reset the stats
this.statsToBeLogged.stats = {};
this.statsToBeLogged.timestamps = [];
};
2014-06-05 11:09:31 +00:00
/**
* Stats processing logic.
*/
StatsCollector.prototype.processStatsReport = function () {
if (!this.baselineStatsReport) {
2014-06-05 11:09:31 +00:00
return;
}
for (var idx in this.currentStatsReport) {
var now = this.currentStatsReport[idx];
if (now.stat('googAvailableReceiveBandwidth') ||
now.stat('googAvailableSendBandwidth'))
{
PeerStats.bandwidth = {
"download": Math.round(
(now.stat('googAvailableReceiveBandwidth')) / 1000),
"upload": Math.round(
(now.stat('googAvailableSendBandwidth')) / 1000)
};
}
if(now.type == 'googCandidatePair')
2014-06-05 11:09:31 +00:00
{
var ip = now.stat('googRemoteAddress');
var type = now.stat("googTransportType");
var localIP = now.stat("googLocalAddress");
var active = now.stat("googActiveConnection");
if(!ip || !type || !localIP || active != "true")
continue;
var addressSaved = false;
for(var i = 0; i < PeerStats.transport.length; i++)
{
if(PeerStats.transport[i].ip == ip &&
PeerStats.transport[i].type == type &&
PeerStats.transport[i].localip == localIP)
{
addressSaved = true;
}
}
if(addressSaved)
continue;
PeerStats.transport.push({localip: localIP, ip: ip, type: type});
2014-06-05 11:09:31 +00:00
continue;
}
if (now.type != 'ssrc') {
continue;
}
var before = this.baselineStatsReport[idx];
if (!before) {
2014-06-05 11:09:31 +00:00
console.warn(now.stat('ssrc') + ' not enough data');
continue;
}
var ssrc = now.stat('ssrc');
var jid = ssrc2jid[ssrc];
if (!jid) {
2014-06-05 11:09:31 +00:00
console.warn("No jid for ssrc: " + ssrc);
continue;
}
var jidStats = this.jid2stats[jid];
if (!jidStats) {
2014-06-05 11:09:31 +00:00
jidStats = new PeerStats();
this.jid2stats[jid] = jidStats;
}
var isDownloadStream = true;
2014-06-05 11:09:31 +00:00
var key = 'packetsReceived';
if (!now.stat(key))
{
isDownloadStream = false;
2014-06-05 11:09:31 +00:00
key = 'packetsSent';
if (!now.stat(key))
{
console.error("No packetsReceived nor packetSent stat found");
this.stop();
return;
}
}
var packetsNow = now.stat(key);
if(!packetsNow || packetsNow < 0)
packetsNow = 0;
2014-06-05 11:09:31 +00:00
var packetsBefore = before.stat(key);
if(!packetsBefore || packetsBefore < 0)
packetsBefore = 0;
2014-06-05 11:09:31 +00:00
var packetRate = packetsNow - packetsBefore;
if(!packetRate || packetRate < 0)
packetRate = 0;
2014-06-05 11:09:31 +00:00
var currentLoss = now.stat('packetsLost');
if(!currentLoss || currentLoss < 0)
currentLoss = 0;
2014-06-05 11:09:31 +00:00
var previousLoss = before.stat('packetsLost');
if(!previousLoss || previousLoss < 0)
previousLoss = 0;
2014-06-05 11:09:31 +00:00
var lossRate = currentLoss - previousLoss;
if(!lossRate || lossRate < 0)
lossRate = 0;
2014-06-05 11:09:31 +00:00
var packetsTotal = (packetRate + lossRate);
jidStats.setSsrcLoss(ssrc,
{"packetsTotal": packetsTotal,
"packetsLost": lossRate,
"isDownloadStream": isDownloadStream});
var bytesReceived = 0, bytesSent = 0;
if(now.stat("bytesReceived"))
{
bytesReceived = now.stat("bytesReceived") -
before.stat("bytesReceived");
}
if(now.stat("bytesSent"))
{
bytesSent = now.stat("bytesSent") - before.stat("bytesSent");
}
var time = Math.round((now.timestamp - before.timestamp) / 1000);
if(bytesReceived <= 0 || time <= 0)
{
bytesReceived = 0;
}
else
{
bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
}
if(bytesSent <= 0 || time <= 0)
{
bytesSent = 0;
}
else
{
bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
}
jidStats.setSsrcBitrate(ssrc, {
"download": bytesReceived,
"upload": bytesSent});
var resolution = {height: null, width: null};
if(now.stat("googFrameHeightReceived") &&
now.stat("googFrameWidthReceived"))
{
resolution.height = now.stat("googFrameHeightReceived");
resolution.width = now.stat("googFrameWidthReceived");
}
else if(now.stat("googFrameHeightSent") &&
now.stat("googFrameWidthSent"))
{
resolution.height = now.stat("googFrameHeightSent");
resolution.width = now.stat("googFrameWidthSent");
}
if(resolution.height && resolution.width)
{
jidStats.setSsrcResolution(ssrc, resolution);
}
else
{
jidStats.setSsrcResolution(ssrc, null);
}
2014-06-05 11:09:31 +00:00
}
var self = this;
// Jid stats
var totalPackets = {download: 0, upload: 0};
var lostPackets = {download: 0, upload: 0};
var bitrateDownload = 0;
var bitrateUpload = 0;
var resolutions = {};
Object.keys(this.jid2stats).forEach(
2014-06-05 11:09:31 +00:00
function (jid)
{
Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
function (ssrc)
2014-06-05 11:09:31 +00:00
{
var type = "upload";
if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream)
type = "download";
totalPackets[type] +=
self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal;
lostPackets[type] +=
self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost;
}
);
Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach(
function (ssrc) {
bitrateDownload +=
self.jid2stats[jid].ssrc2bitrate[ssrc].download;
bitrateUpload +=
self.jid2stats[jid].ssrc2bitrate[ssrc].upload;
2014-06-05 11:09:31 +00:00
}
);
resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
2014-06-05 11:09:31 +00:00
}
);
PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload};
PeerStats.packetLoss = {
total:
calculatePacketLoss(lostPackets.download + lostPackets.upload,
totalPackets.download + totalPackets.upload),
download:
calculatePacketLoss(lostPackets.download, totalPackets.download),
upload:
calculatePacketLoss(lostPackets.upload, totalPackets.upload)
};
this.statsUpdateCallback(
{
"bitrate": PeerStats.bitrate,
"packetLoss": PeerStats.packetLoss,
"bandwidth": PeerStats.bandwidth,
"resolution": resolutions,
"transport": PeerStats.transport
});
PeerStats.transport = [];
};
/**
* Stats processing logic.
*/
StatsCollector.prototype.processAudioLevelReport = function ()
{
if (!this.baselineAudioLevelsReport)
2014-06-05 11:09:31 +00:00
{
return;
2014-06-05 11:09:31 +00:00
}
for (var idx in this.currentAudioLevelsReport)
{
var now = this.currentAudioLevelsReport[idx];
if (now.type != 'ssrc')
{
continue;
}
var before = this.baselineAudioLevelsReport[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.audioLevelsUpdateCallback(jid, audioLevel);
}
}
};