1290 lines
36 KiB
JavaScript
1290 lines
36 KiB
JavaScript
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.statistics=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
/**
|
|
* Provides statistics for the local stream.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Size of the webaudio analizer buffer.
|
|
* @type {number}
|
|
*/
|
|
var WEBAUDIO_ANALIZER_FFT_SIZE = 2048;
|
|
|
|
/**
|
|
* Value of the webaudio analizer smoothing time parameter.
|
|
* @type {number}
|
|
*/
|
|
var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8;
|
|
|
|
/**
|
|
* Converts time domain data array to audio level.
|
|
* @param array the time domain data array.
|
|
* @returns {number} the audio level
|
|
*/
|
|
function timeDomainDataToAudioLevel(samples) {
|
|
|
|
var maxVolume = 0;
|
|
|
|
var length = samples.length;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
if (maxVolume < samples[i])
|
|
maxVolume = samples[i];
|
|
}
|
|
|
|
return parseFloat(((maxVolume - 127) / 128).toFixed(3));
|
|
};
|
|
|
|
/**
|
|
* Animates audio level change
|
|
* @param newLevel the new audio level
|
|
* @param lastLevel the last audio level
|
|
* @returns {Number} the audio level to be set
|
|
*/
|
|
function animateLevel(newLevel, lastLevel)
|
|
{
|
|
var value = 0;
|
|
var diff = lastLevel - newLevel;
|
|
if(diff > 0.2)
|
|
{
|
|
value = lastLevel - 0.2;
|
|
}
|
|
else if(diff < -0.4)
|
|
{
|
|
value = lastLevel + 0.4;
|
|
}
|
|
else
|
|
{
|
|
value = newLevel;
|
|
}
|
|
|
|
return parseFloat(value.toFixed(3));
|
|
}
|
|
|
|
|
|
/**
|
|
* <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
|
|
*
|
|
* @param stream the local stream
|
|
* @param interval stats refresh interval given in ms.
|
|
* @param {function(LocalStatsCollector)} updateCallback the callback called on stats
|
|
* update.
|
|
* @constructor
|
|
*/
|
|
function LocalStatsCollector(stream, interval, statisticsService, eventEmitter) {
|
|
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
this.stream = stream;
|
|
this.intervalId = null;
|
|
this.intervalMilis = interval;
|
|
this.eventEmitter = eventEmitter;
|
|
this.audioLevel = 0;
|
|
this.statisticsService = statisticsService;
|
|
}
|
|
|
|
/**
|
|
* Starts the collecting the statistics.
|
|
*/
|
|
LocalStatsCollector.prototype.start = function () {
|
|
if (!window.AudioContext)
|
|
return;
|
|
|
|
var context = new AudioContext();
|
|
var analyser = context.createAnalyser();
|
|
analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
|
|
analyser.fftSize = WEBAUDIO_ANALIZER_FFT_SIZE;
|
|
|
|
|
|
var source = context.createMediaStreamSource(this.stream);
|
|
source.connect(analyser);
|
|
|
|
|
|
var self = this;
|
|
|
|
this.intervalId = setInterval(
|
|
function () {
|
|
var array = new Uint8Array(analyser.frequencyBinCount);
|
|
analyser.getByteTimeDomainData(array);
|
|
var audioLevel = timeDomainDataToAudioLevel(array);
|
|
if(audioLevel != self.audioLevel) {
|
|
self.audioLevel = animateLevel(audioLevel, self.audioLevel);
|
|
self.eventEmitter.emit(
|
|
"statistics.audioLevel",
|
|
self.statisticsService.LOCAL_JID,
|
|
self.audioLevel);
|
|
}
|
|
},
|
|
this.intervalMilis
|
|
);
|
|
|
|
};
|
|
|
|
/**
|
|
* Stops collecting the statistics.
|
|
*/
|
|
LocalStatsCollector.prototype.stop = function () {
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
this.intervalId = null;
|
|
}
|
|
};
|
|
|
|
module.exports = LocalStatsCollector;
|
|
},{}],2:[function(require,module,exports){
|
|
/* global focusMucJid, ssrc2jid */
|
|
/* jshint -W117 */
|
|
/**
|
|
* 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
|
|
*/
|
|
function calculatePacketLoss(lostPackets, totalPackets) {
|
|
if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0)
|
|
return 0;
|
|
return Math.round((lostPackets/totalPackets)*100);
|
|
}
|
|
|
|
function getStatValue(item, name) {
|
|
if(!keyMap[RTC.getBrowserType()][name])
|
|
throw "The property isn't supported!";
|
|
var key = keyMap[RTC.getBrowserType()][name];
|
|
return RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_CHROME? item.stat(key) : item[key];
|
|
}
|
|
|
|
/**
|
|
* Peer statistics data holder.
|
|
* @constructor
|
|
*/
|
|
function PeerStats()
|
|
{
|
|
this.ssrc2Loss = {};
|
|
this.ssrc2AudioLevel = {};
|
|
this.ssrc2bitrate = {};
|
|
this.ssrc2resolution = {};
|
|
}
|
|
|
|
/**
|
|
* The bandwidth
|
|
* @type {{}}
|
|
*/
|
|
PeerStats.bandwidth = {};
|
|
|
|
/**
|
|
* The bit rate
|
|
* @type {{}}
|
|
*/
|
|
PeerStats.bitrate = {};
|
|
|
|
|
|
|
|
/**
|
|
* The packet loss rate
|
|
* @type {{}}
|
|
*/
|
|
PeerStats.packetLoss = null;
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
if(this.ssrc2bitrate[ssrc])
|
|
{
|
|
this.ssrc2bitrate[ssrc].download += bitrate.download;
|
|
this.ssrc2bitrate[ssrc].upload += bitrate.upload;
|
|
}
|
|
else {
|
|
this.ssrc2bitrate[ssrc] = bitrate;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
PeerStats.transport = [];
|
|
|
|
|
|
/**
|
|
* <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.
|
|
*
|
|
* @param peerconnection webRTC peer connection object.
|
|
* @param interval stats refresh interval given in ms.
|
|
* @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
|
|
* called on stats update.
|
|
* @constructor
|
|
*/
|
|
function StatsCollector(peerconnection, audioLevelsInterval, statsInterval, eventEmitter)
|
|
{
|
|
this.peerconnection = peerconnection;
|
|
this.baselineAudioLevelsReport = null;
|
|
this.currentAudioLevelsReport = null;
|
|
this.currentStatsReport = null;
|
|
this.baselineStatsReport = null;
|
|
this.audioLevelsIntervalId = null;
|
|
this.eventEmitter = eventEmitter;
|
|
|
|
/**
|
|
* 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: {}
|
|
};
|
|
|
|
// Updates stats interval
|
|
this.audioLevelsIntervalMilis = audioLevelsInterval;
|
|
|
|
this.statsIntervalId = null;
|
|
this.statsIntervalMilis = statsInterval;
|
|
// Map of jids to PeerStats
|
|
this.jid2stats = {};
|
|
}
|
|
|
|
module.exports = StatsCollector;
|
|
|
|
/**
|
|
* Stops stats updates.
|
|
*/
|
|
StatsCollector.prototype.stop = function ()
|
|
{
|
|
if (this.audioLevelsIntervalId)
|
|
{
|
|
clearInterval(this.audioLevelsIntervalId);
|
|
this.audioLevelsIntervalId = null;
|
|
clearInterval(this.statsIntervalId);
|
|
this.statsIntervalId = null;
|
|
clearInterval(this.logStatsIntervalId);
|
|
this.logStatsIntervalId = null;
|
|
clearInterval(this.gatherStatsIntervalId);
|
|
this.gatherStatsIntervalId = 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.audioLevelsIntervalId = setInterval(
|
|
function ()
|
|
{
|
|
// Interval updates
|
|
self.peerconnection.getStats(
|
|
function (report)
|
|
{
|
|
var results = null;
|
|
if(!report || !report.result || typeof report.result != 'function')
|
|
{
|
|
results = report;
|
|
}
|
|
else
|
|
{
|
|
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 = null;
|
|
if(!report || !report.result || typeof report.result != 'function')
|
|
{
|
|
//firefox
|
|
results = report;
|
|
}
|
|
else
|
|
{
|
|
//chrome
|
|
results = report.result();
|
|
}
|
|
//console.error("Got interval report", results);
|
|
self.currentStatsReport = results;
|
|
try
|
|
{
|
|
self.processStatsReport();
|
|
}
|
|
catch (e)
|
|
{
|
|
console.error("Unsupported key:" + e, e);
|
|
}
|
|
|
|
self.baselineStatsReport = self.currentStatsReport;
|
|
},
|
|
self.errorCallback
|
|
);
|
|
},
|
|
self.statsIntervalMilis
|
|
);
|
|
|
|
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));
|
|
});
|
|
});
|
|
};
|
|
|
|
StatsCollector.prototype.logStats = function () {
|
|
if (!focusMucJid) {
|
|
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: focusMucJid, 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 = [];
|
|
};
|
|
var keyMap = {};
|
|
keyMap[RTCBrowserType.RTC_BROWSER_FIREFOX] = {
|
|
"ssrc": "ssrc",
|
|
"packetsReceived": "packetsReceived",
|
|
"packetsLost": "packetsLost",
|
|
"packetsSent": "packetsSent",
|
|
"bytesReceived": "bytesReceived",
|
|
"bytesSent": "bytesSent"
|
|
};
|
|
keyMap[RTCBrowserType.RTC_BROWSER_CHROME] = {
|
|
"receiveBandwidth": "googAvailableReceiveBandwidth",
|
|
"sendBandwidth": "googAvailableSendBandwidth",
|
|
"remoteAddress": "googRemoteAddress",
|
|
"transportType": "googTransportType",
|
|
"localAddress": "googLocalAddress",
|
|
"activeConnection": "googActiveConnection",
|
|
"ssrc": "ssrc",
|
|
"packetsReceived": "packetsReceived",
|
|
"packetsSent": "packetsSent",
|
|
"packetsLost": "packetsLost",
|
|
"bytesReceived": "bytesReceived",
|
|
"bytesSent": "bytesSent",
|
|
"googFrameHeightReceived": "googFrameHeightReceived",
|
|
"googFrameWidthReceived": "googFrameWidthReceived",
|
|
"googFrameHeightSent": "googFrameHeightSent",
|
|
"googFrameWidthSent": "googFrameWidthSent",
|
|
"audioInputLevel": "audioInputLevel",
|
|
"audioOutputLevel": "audioOutputLevel"
|
|
};
|
|
|
|
|
|
/**
|
|
* Stats processing logic.
|
|
*/
|
|
StatsCollector.prototype.processStatsReport = function () {
|
|
if (!this.baselineStatsReport) {
|
|
return;
|
|
}
|
|
|
|
for (var idx in this.currentStatsReport) {
|
|
var now = this.currentStatsReport[idx];
|
|
try {
|
|
if (getStatValue(now, 'receiveBandwidth') ||
|
|
getStatValue(now, 'sendBandwidth')) {
|
|
PeerStats.bandwidth = {
|
|
"download": Math.round(
|
|
(getStatValue(now, 'receiveBandwidth')) / 1000),
|
|
"upload": Math.round(
|
|
(getStatValue(now, 'sendBandwidth')) / 1000)
|
|
};
|
|
}
|
|
}
|
|
catch(e){/*not supported*/}
|
|
|
|
if(now.type == 'googCandidatePair')
|
|
{
|
|
var ip, type, localIP, active;
|
|
try {
|
|
ip = getStatValue(now, 'remoteAddress');
|
|
type = getStatValue(now, "transportType");
|
|
localIP = getStatValue(now, "localAddress");
|
|
active = getStatValue(now, "activeConnection");
|
|
}
|
|
catch(e){/*not supported*/}
|
|
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});
|
|
continue;
|
|
}
|
|
|
|
if(now.type == "candidatepair")
|
|
{
|
|
if(now.state == "succeeded")
|
|
continue;
|
|
|
|
var local = this.currentStatsReport[now.localCandidateId];
|
|
var remote = this.currentStatsReport[now.remoteCandidateId];
|
|
PeerStats.transport.push({localip: local.ipAddress + ":" + local.portNumber,
|
|
ip: remote.ipAddress + ":" + remote.portNumber, type: local.transport});
|
|
|
|
}
|
|
|
|
if (now.type != 'ssrc' && now.type != "outboundrtp" &&
|
|
now.type != "inboundrtp") {
|
|
continue;
|
|
}
|
|
|
|
var before = this.baselineStatsReport[idx];
|
|
if (!before) {
|
|
console.warn(getStatValue(now, 'ssrc') + ' not enough data');
|
|
continue;
|
|
}
|
|
|
|
var ssrc = getStatValue(now, 'ssrc');
|
|
if(!ssrc)
|
|
continue;
|
|
var jid = ssrc2jid[ssrc];
|
|
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
|
console.warn("No jid for ssrc: " + ssrc);
|
|
continue;
|
|
}
|
|
|
|
var jidStats = this.jid2stats[jid];
|
|
if (!jidStats) {
|
|
jidStats = new PeerStats();
|
|
this.jid2stats[jid] = jidStats;
|
|
}
|
|
|
|
|
|
var isDownloadStream = true;
|
|
var key = 'packetsReceived';
|
|
if (!getStatValue(now, key))
|
|
{
|
|
isDownloadStream = false;
|
|
key = 'packetsSent';
|
|
if (!getStatValue(now, key))
|
|
{
|
|
console.warn("No packetsReceived nor packetSent stat found");
|
|
continue;
|
|
}
|
|
}
|
|
var packetsNow = getStatValue(now, key);
|
|
if(!packetsNow || packetsNow < 0)
|
|
packetsNow = 0;
|
|
|
|
var packetsBefore = getStatValue(before, key);
|
|
if(!packetsBefore || packetsBefore < 0)
|
|
packetsBefore = 0;
|
|
var packetRate = packetsNow - packetsBefore;
|
|
if(!packetRate || packetRate < 0)
|
|
packetRate = 0;
|
|
var currentLoss = getStatValue(now, 'packetsLost');
|
|
if(!currentLoss || currentLoss < 0)
|
|
currentLoss = 0;
|
|
var previousLoss = getStatValue(before, 'packetsLost');
|
|
if(!previousLoss || previousLoss < 0)
|
|
previousLoss = 0;
|
|
var lossRate = currentLoss - previousLoss;
|
|
if(!lossRate || lossRate < 0)
|
|
lossRate = 0;
|
|
var packetsTotal = (packetRate + lossRate);
|
|
|
|
jidStats.setSsrcLoss(ssrc,
|
|
{"packetsTotal": packetsTotal,
|
|
"packetsLost": lossRate,
|
|
"isDownloadStream": isDownloadStream});
|
|
|
|
|
|
var bytesReceived = 0, bytesSent = 0;
|
|
if(getStatValue(now, "bytesReceived"))
|
|
{
|
|
bytesReceived = getStatValue(now, "bytesReceived") -
|
|
getStatValue(before, "bytesReceived");
|
|
}
|
|
|
|
if(getStatValue(now, "bytesSent"))
|
|
{
|
|
bytesSent = getStatValue(now, "bytesSent") -
|
|
getStatValue(before, "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};
|
|
try {
|
|
if (getStatValue(now, "googFrameHeightReceived") &&
|
|
getStatValue(now, "googFrameWidthReceived")) {
|
|
resolution.height = getStatValue(now, "googFrameHeightReceived");
|
|
resolution.width = getStatValue(now, "googFrameWidthReceived");
|
|
}
|
|
else if (getStatValue(now, "googFrameHeightSent") &&
|
|
getStatValue(now, "googFrameWidthSent")) {
|
|
resolution.height = getStatValue(now, "googFrameHeightSent");
|
|
resolution.width = getStatValue(now, "googFrameWidthSent");
|
|
}
|
|
}
|
|
catch(e){/*not supported*/}
|
|
|
|
if(resolution.height && resolution.width)
|
|
{
|
|
jidStats.setSsrcResolution(ssrc, resolution);
|
|
}
|
|
else
|
|
{
|
|
jidStats.setSsrcResolution(ssrc, null);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
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(
|
|
function (jid)
|
|
{
|
|
Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
|
|
function (ssrc)
|
|
{
|
|
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;
|
|
|
|
delete self.jid2stats[jid].ssrc2bitrate[ssrc];
|
|
}
|
|
);
|
|
resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
|
|
}
|
|
);
|
|
|
|
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.eventEmitter.emit("statistics.connectionstats",
|
|
{
|
|
"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)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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(getStatValue(now, 'ssrc') + ' not enough data');
|
|
continue;
|
|
}
|
|
|
|
var ssrc = getStatValue(now, 'ssrc');
|
|
var jid = ssrc2jid[ssrc];
|
|
if (!jid && (Date.now() - now.timestamp) < 3000)
|
|
{
|
|
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 = null;
|
|
|
|
try {
|
|
audioLevel = getStatValue(now, 'audioInputLevel');
|
|
if (!audioLevel)
|
|
audioLevel = getStatValue(now, 'audioOutputLevel');
|
|
}
|
|
catch(e) {/*not supported*/
|
|
console.warn("Audio Levels are not available in the statistics.");
|
|
clearInterval(this.audioLevelsIntervalId);
|
|
return;
|
|
}
|
|
|
|
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.eventEmitter.emit("statistics.audioLevel", jid, audioLevel);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
};
|
|
},{}],3:[function(require,module,exports){
|
|
/**
|
|
* Created by hristo on 8/4/14.
|
|
*/
|
|
var LocalStats = require("./LocalStatsCollector.js");
|
|
var RTPStats = require("./RTPStatsCollector.js");
|
|
var EventEmitter = require("events");
|
|
//These lines should be uncommented when require works in app.js
|
|
//var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
|
//var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
|
|
//var XMPPEvents = require("../service/xmpp/XMPPEvents");
|
|
|
|
var eventEmitter = new EventEmitter();
|
|
|
|
var localStats = null;
|
|
|
|
var rtpStats = null;
|
|
|
|
function stopLocal()
|
|
{
|
|
if(localStats)
|
|
{
|
|
localStats.stop();
|
|
localStats = null;
|
|
}
|
|
}
|
|
|
|
function stopRemote()
|
|
{
|
|
if(rtpStats)
|
|
{
|
|
rtpStats.stop();
|
|
eventEmitter.emit("statistics.stop");
|
|
rtpStats = null;
|
|
}
|
|
}
|
|
|
|
function startRemoteStats (peerconnection) {
|
|
if (config.enableRtpStats)
|
|
{
|
|
if(rtpStats)
|
|
{
|
|
rtpStats.stop();
|
|
rtpStats = null;
|
|
}
|
|
|
|
rtpStats = new RTPStats(peerconnection, 200, 2000, eventEmitter);
|
|
rtpStats.start();
|
|
}
|
|
|
|
}
|
|
|
|
function onStreamCreated(stream)
|
|
{
|
|
if(stream.getOriginalStream().getAudioTracks().length === 0)
|
|
return;
|
|
|
|
localStats = new LocalStats(stream.getOriginalStream(), 100, statistics,
|
|
eventEmitter);
|
|
localStats.start();
|
|
}
|
|
|
|
|
|
var statistics =
|
|
{
|
|
/**
|
|
* Indicates that this audio level is for local jid.
|
|
* @type {string}
|
|
*/
|
|
LOCAL_JID: 'local',
|
|
|
|
addAudioLevelListener: function(listener)
|
|
{
|
|
eventEmitter.on("statistics.audioLevel", listener);
|
|
},
|
|
|
|
removeAudioLevelListener: function(listener)
|
|
{
|
|
eventEmitter.removeListener("statistics.audioLevel", listener);
|
|
},
|
|
|
|
addConnectionStatsListener: function(listener)
|
|
{
|
|
eventEmitter.on("statistics.connectionstats", listener);
|
|
},
|
|
|
|
removeConnectionStatsListener: function(listener)
|
|
{
|
|
eventEmitter.removeListener("statistics.connectionstats", listener);
|
|
},
|
|
|
|
|
|
addRemoteStatsStopListener: function(listener)
|
|
{
|
|
eventEmitter.on("statistics.stop", listener);
|
|
},
|
|
|
|
removeRemoteStatsStopListener: function(listener)
|
|
{
|
|
eventEmitter.removeListener("statistics.stop", listener);
|
|
},
|
|
|
|
stop: function () {
|
|
stopLocal();
|
|
stopRemote();
|
|
if(eventEmitter)
|
|
{
|
|
eventEmitter.removeAllListeners();
|
|
}
|
|
},
|
|
|
|
stopRemoteStatistics: function()
|
|
{
|
|
stopRemote();
|
|
},
|
|
|
|
onConferenceCreated: function (event) {
|
|
startRemoteStats(event.peerconnection);
|
|
},
|
|
|
|
onDisposeConference: function (onUnload) {
|
|
stopRemote();
|
|
if(onUnload) {
|
|
stopLocal();
|
|
eventEmitter.removeAllListeners();
|
|
}
|
|
},
|
|
|
|
start: function () {
|
|
this.addConnectionStatsListener(connection_quality.updateLocalStats);
|
|
this.addRemoteStatsStopListener(connection_quality.stopSendingStats);
|
|
RTC.addStreamListener(onStreamCreated,
|
|
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = statistics;
|
|
},{"./LocalStatsCollector.js":1,"./RTPStatsCollector.js":2,"events":4}],4:[function(require,module,exports){
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
function EventEmitter() {
|
|
this._events = this._events || {};
|
|
this._maxListeners = this._maxListeners || undefined;
|
|
}
|
|
module.exports = EventEmitter;
|
|
|
|
// Backwards-compat with node 0.10.x
|
|
EventEmitter.EventEmitter = EventEmitter;
|
|
|
|
EventEmitter.prototype._events = undefined;
|
|
EventEmitter.prototype._maxListeners = undefined;
|
|
|
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
// added to it. This is a useful default which helps finding memory leaks.
|
|
EventEmitter.defaultMaxListeners = 10;
|
|
|
|
// Obviously not all Emitters should be limited to 10. This function allows
|
|
// that to be increased. Set to zero for unlimited.
|
|
EventEmitter.prototype.setMaxListeners = function(n) {
|
|
if (!isNumber(n) || n < 0 || isNaN(n))
|
|
throw TypeError('n must be a positive number');
|
|
this._maxListeners = n;
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.emit = function(type) {
|
|
var er, handler, len, args, i, listeners;
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// If there is no 'error' event listener then throw.
|
|
if (type === 'error') {
|
|
if (!this._events.error ||
|
|
(isObject(this._events.error) && !this._events.error.length)) {
|
|
er = arguments[1];
|
|
if (er instanceof Error) {
|
|
throw er; // Unhandled 'error' event
|
|
} else {
|
|
throw TypeError('Uncaught, unspecified "error" event.');
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
handler = this._events[type];
|
|
|
|
if (isUndefined(handler))
|
|
return false;
|
|
|
|
if (isFunction(handler)) {
|
|
switch (arguments.length) {
|
|
// fast cases
|
|
case 1:
|
|
handler.call(this);
|
|
break;
|
|
case 2:
|
|
handler.call(this, arguments[1]);
|
|
break;
|
|
case 3:
|
|
handler.call(this, arguments[1], arguments[2]);
|
|
break;
|
|
// slower
|
|
default:
|
|
len = arguments.length;
|
|
args = new Array(len - 1);
|
|
for (i = 1; i < len; i++)
|
|
args[i - 1] = arguments[i];
|
|
handler.apply(this, args);
|
|
}
|
|
} else if (isObject(handler)) {
|
|
len = arguments.length;
|
|
args = new Array(len - 1);
|
|
for (i = 1; i < len; i++)
|
|
args[i - 1] = arguments[i];
|
|
|
|
listeners = handler.slice();
|
|
len = listeners.length;
|
|
for (i = 0; i < len; i++)
|
|
listeners[i].apply(this, args);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
EventEmitter.prototype.addListener = function(type, listener) {
|
|
var m;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// To avoid recursion in the case that type === "newListener"! Before
|
|
// adding it to the listeners, first emit "newListener".
|
|
if (this._events.newListener)
|
|
this.emit('newListener', type,
|
|
isFunction(listener.listener) ?
|
|
listener.listener : listener);
|
|
|
|
if (!this._events[type])
|
|
// Optimize the case of one listener. Don't need the extra array object.
|
|
this._events[type] = listener;
|
|
else if (isObject(this._events[type]))
|
|
// If we've already got an array, just append.
|
|
this._events[type].push(listener);
|
|
else
|
|
// Adding the second element, need to change to array.
|
|
this._events[type] = [this._events[type], listener];
|
|
|
|
// Check for listener leak
|
|
if (isObject(this._events[type]) && !this._events[type].warned) {
|
|
var m;
|
|
if (!isUndefined(this._maxListeners)) {
|
|
m = this._maxListeners;
|
|
} else {
|
|
m = EventEmitter.defaultMaxListeners;
|
|
}
|
|
|
|
if (m && m > 0 && this._events[type].length > m) {
|
|
this._events[type].warned = true;
|
|
console.error('(node) warning: possible EventEmitter memory ' +
|
|
'leak detected. %d listeners added. ' +
|
|
'Use emitter.setMaxListeners() to increase limit.',
|
|
this._events[type].length);
|
|
if (typeof console.trace === 'function') {
|
|
// not supported in IE 10
|
|
console.trace();
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
|
|
EventEmitter.prototype.once = function(type, listener) {
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
var fired = false;
|
|
|
|
function g() {
|
|
this.removeListener(type, g);
|
|
|
|
if (!fired) {
|
|
fired = true;
|
|
listener.apply(this, arguments);
|
|
}
|
|
}
|
|
|
|
g.listener = listener;
|
|
this.on(type, g);
|
|
|
|
return this;
|
|
};
|
|
|
|
// emits a 'removeListener' event iff the listener was removed
|
|
EventEmitter.prototype.removeListener = function(type, listener) {
|
|
var list, position, length, i;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events || !this._events[type])
|
|
return this;
|
|
|
|
list = this._events[type];
|
|
length = list.length;
|
|
position = -1;
|
|
|
|
if (list === listener ||
|
|
(isFunction(list.listener) && list.listener === listener)) {
|
|
delete this._events[type];
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
|
|
} else if (isObject(list)) {
|
|
for (i = length; i-- > 0;) {
|
|
if (list[i] === listener ||
|
|
(list[i].listener && list[i].listener === listener)) {
|
|
position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (position < 0)
|
|
return this;
|
|
|
|
if (list.length === 1) {
|
|
list.length = 0;
|
|
delete this._events[type];
|
|
} else {
|
|
list.splice(position, 1);
|
|
}
|
|
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
var key, listeners;
|
|
|
|
if (!this._events)
|
|
return this;
|
|
|
|
// not listening for removeListener, no need to emit
|
|
if (!this._events.removeListener) {
|
|
if (arguments.length === 0)
|
|
this._events = {};
|
|
else if (this._events[type])
|
|
delete this._events[type];
|
|
return this;
|
|
}
|
|
|
|
// emit removeListener for all listeners on all events
|
|
if (arguments.length === 0) {
|
|
for (key in this._events) {
|
|
if (key === 'removeListener') continue;
|
|
this.removeAllListeners(key);
|
|
}
|
|
this.removeAllListeners('removeListener');
|
|
this._events = {};
|
|
return this;
|
|
}
|
|
|
|
listeners = this._events[type];
|
|
|
|
if (isFunction(listeners)) {
|
|
this.removeListener(type, listeners);
|
|
} else {
|
|
// LIFO order
|
|
while (listeners.length)
|
|
this.removeListener(type, listeners[listeners.length - 1]);
|
|
}
|
|
delete this._events[type];
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.listeners = function(type) {
|
|
var ret;
|
|
if (!this._events || !this._events[type])
|
|
ret = [];
|
|
else if (isFunction(this._events[type]))
|
|
ret = [this._events[type]];
|
|
else
|
|
ret = this._events[type].slice();
|
|
return ret;
|
|
};
|
|
|
|
EventEmitter.listenerCount = function(emitter, type) {
|
|
var ret;
|
|
if (!emitter._events || !emitter._events[type])
|
|
ret = 0;
|
|
else if (isFunction(emitter._events[type]))
|
|
ret = 1;
|
|
else
|
|
ret = emitter._events[type].length;
|
|
return ret;
|
|
};
|
|
|
|
function isFunction(arg) {
|
|
return typeof arg === 'function';
|
|
}
|
|
|
|
function isNumber(arg) {
|
|
return typeof arg === 'number';
|
|
}
|
|
|
|
function isObject(arg) {
|
|
return typeof arg === 'object' && arg !== null;
|
|
}
|
|
|
|
function isUndefined(arg) {
|
|
return arg === void 0;
|
|
}
|
|
|
|
},{}]},{},[3])(3)
|
|
}); |