610 lines
22 KiB
JavaScript
610 lines
22 KiB
JavaScript
|
// Derived from Chromium WebRTC Internals Dashboard - see Acknowledgements for full license details
|
|||
|
|
|||
|
const CalculatorModifier = Object.freeze({
|
|||
|
kNone: Object.freeze({postfix: '', multiplier: 1}),
|
|||
|
kMillisecondsFromSeconds:
|
|||
|
Object.freeze({postfix: '_in_ms', multiplier: 1000}),
|
|||
|
kBytesToBits: Object.freeze({bitrate: true, multiplier: 8}),
|
|||
|
});
|
|||
|
|
|||
|
class Metric {
|
|||
|
constructor(name, value) {
|
|||
|
this.name = name;
|
|||
|
this.value = value;
|
|||
|
}
|
|||
|
|
|||
|
toString() {
|
|||
|
return '{"' + this.name + '":"' + this.value + '"}';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Represents a companion dictionary to an RTCStats object of an RTCStatsReport.
|
|||
|
// The CalculatedStats object contains additional metrics associated with the
|
|||
|
// original RTCStats object. Typically, the RTCStats object contains
|
|||
|
// accumulative counters, but in chrome://webrc-internals/ we also want graphs
|
|||
|
// for the average rate over the last second, so we have CalculatedStats
|
|||
|
// containing calculated Metrics.
|
|||
|
class CalculatedStats {
|
|||
|
constructor(id) {
|
|||
|
this.id = id;
|
|||
|
// A map Original Name -> Array of Metrics, where Original Name refers to
|
|||
|
// the name of the metric in the original RTCStats object, and the Metrics
|
|||
|
// are calculated metrics. For example, if the original RTCStats report
|
|||
|
// contains framesReceived, and from that we've calculated
|
|||
|
// [framesReceived/s] and [framesReceived-framesDecoded], then there will be
|
|||
|
// a mapping from "framesReceived" to an array of two Metric objects,
|
|||
|
// "[framesReceived/s]" and "[framesReceived-framesDecoded]".
|
|||
|
this.calculatedMetricsByOriginalName = new Map();
|
|||
|
}
|
|||
|
|
|||
|
addCalculatedMetric(originalName, metric) {
|
|||
|
let calculatedMetrics =
|
|||
|
this.calculatedMetricsByOriginalName.get(originalName);
|
|||
|
if (!calculatedMetrics) {
|
|||
|
calculatedMetrics = [];
|
|||
|
this.calculatedMetricsByOriginalName.set(originalName, calculatedMetrics);
|
|||
|
}
|
|||
|
calculatedMetrics.push(metric);
|
|||
|
}
|
|||
|
|
|||
|
// Gets the calculated metrics associated with |originalName| in the order
|
|||
|
// that they were added, or an empty list if there are no associated metrics.
|
|||
|
getCalculatedMetrics(originalName) {
|
|||
|
const calculatedMetrics =
|
|||
|
this.calculatedMetricsByOriginalName.get(originalName);
|
|||
|
if (!calculatedMetrics) {
|
|||
|
return [];
|
|||
|
}
|
|||
|
return calculatedMetrics;
|
|||
|
}
|
|||
|
|
|||
|
toString() {
|
|||
|
let str = '{id:"' + this.id + '"';
|
|||
|
for (const originalName of this.calculatedMetricsByOriginalName.keys()) {
|
|||
|
const calculatedMetrics =
|
|||
|
this.calculatedMetricsByOriginalName.get(originalName);
|
|||
|
str += ',' + originalName + ':[';
|
|||
|
for (let i = 0; i < calculatedMetrics.length; i++) {
|
|||
|
str += calculatedMetrics[i].toString();
|
|||
|
if (i + 1 < calculatedMetrics.length) {
|
|||
|
str += ',';
|
|||
|
}
|
|||
|
str += ']';
|
|||
|
}
|
|||
|
}
|
|||
|
str += '}';
|
|||
|
return str;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Contains the metrics of an RTCStatsReport, as well as calculated metrics
|
|||
|
// associated with metrics from the original report. Convertible to and from the
|
|||
|
// "internal reports" format used by webrtc_internals.js to pass stats from C++
|
|||
|
// to JavaScript.
|
|||
|
export class StatsReport {
|
|||
|
constructor() {
|
|||
|
// Represents an RTCStatsReport. It is a Map RTCStats.id -> RTCStats.
|
|||
|
// https://w3c.github.io/webrtc-pc/#dom-rtcstatsreport
|
|||
|
this.statsById = new Map();
|
|||
|
// RTCStats.id -> CalculatedStats
|
|||
|
this.calculatedStatsById = new Map();
|
|||
|
}
|
|||
|
|
|||
|
// |internalReports| is an array, each element represents an RTCStats object,
|
|||
|
// but the format is a little different from the spec. This is the format:
|
|||
|
// {
|
|||
|
// id: "string",
|
|||
|
// type: "string",
|
|||
|
// stats: {
|
|||
|
// timestamp: <milliseconds>,
|
|||
|
// values: ["member1", value1, "member2", value2...]
|
|||
|
// }
|
|||
|
// }
|
|||
|
static fromInternalsReportList(internalReports) {
|
|||
|
const result = new StatsReport();
|
|||
|
internalReports.forEach(internalReport => {
|
|||
|
if (!internalReport.stats || !internalReport.stats.values) {
|
|||
|
return; // continue;
|
|||
|
}
|
|||
|
const stats = {
|
|||
|
id: internalReport.id,
|
|||
|
type: internalReport.type,
|
|||
|
timestamp: internalReport.stats.timestamp / 1000.0 // ms -> s
|
|||
|
};
|
|||
|
const values = internalReport.stats.values;
|
|||
|
for (let i = 0; i < values.length; i += 2) {
|
|||
|
// Metric "name: value".
|
|||
|
stats[values[i]] = values[i + 1];
|
|||
|
}
|
|||
|
result.statsById.set(stats.id, stats);
|
|||
|
});
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
toInternalsReportList() {
|
|||
|
const result = [];
|
|||
|
for (const stats of this.statsById.values()) {
|
|||
|
const internalReport = {
|
|||
|
id: stats.id,
|
|||
|
type: stats.type,
|
|||
|
stats: {
|
|||
|
timestamp: stats.timestamp * 1000.0, // s -> ms
|
|||
|
values: []
|
|||
|
}
|
|||
|
};
|
|||
|
Object.keys(stats).forEach(metricName => {
|
|||
|
if (metricName === 'id' || metricName === 'type' ||
|
|||
|
metricName === 'timestamp') {
|
|||
|
return; // continue;
|
|||
|
}
|
|||
|
internalReport.stats.values.push(metricName);
|
|||
|
internalReport.stats.values.push(stats[metricName]);
|
|||
|
const calculatedMetrics =
|
|||
|
this.getCalculatedMetrics(stats.id, metricName);
|
|||
|
calculatedMetrics.forEach(calculatedMetric => {
|
|||
|
internalReport.stats.values.push(calculatedMetric.name);
|
|||
|
// Treat calculated metrics that are undefined as 0 to ensure graphs
|
|||
|
// can be created anyway.
|
|||
|
internalReport.stats.values.push(
|
|||
|
calculatedMetric.value ? calculatedMetric.value : 0);
|
|||
|
});
|
|||
|
});
|
|||
|
result.push(internalReport);
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
toString() {
|
|||
|
let str = '';
|
|||
|
for (const stats of this.statsById.values()) {
|
|||
|
if (str !== '') {
|
|||
|
str += ',';
|
|||
|
}
|
|||
|
str += JSON.stringify(stats);
|
|||
|
}
|
|||
|
let str2 = '';
|
|||
|
for (const stats of this.calculatedStatsById.values()) {
|
|||
|
if (str2 !== '') {
|
|||
|
str2 += ',';
|
|||
|
}
|
|||
|
str2 += stats.toString();
|
|||
|
}
|
|||
|
return '[original:' + str + '],calculated:[' + str2 + ']';
|
|||
|
}
|
|||
|
|
|||
|
get(id) {
|
|||
|
return this.statsById.get(id);
|
|||
|
}
|
|||
|
|
|||
|
getByType(type) {
|
|||
|
const result = [];
|
|||
|
for (const stats of this.statsById.values()) {
|
|||
|
if (stats.type === type) {
|
|||
|
result.push(stats);
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
addCalculatedMetric(id, insertAtOriginalMetricName, name, value) {
|
|||
|
let calculatedStats = this.calculatedStatsById.get(id);
|
|||
|
if (!calculatedStats) {
|
|||
|
calculatedStats = new CalculatedStats(id);
|
|||
|
this.calculatedStatsById.set(id, calculatedStats);
|
|||
|
}
|
|||
|
calculatedStats.addCalculatedMetric(
|
|||
|
insertAtOriginalMetricName, new Metric(name, value));
|
|||
|
}
|
|||
|
|
|||
|
getCalculatedMetrics(id, originalMetricName) {
|
|||
|
const calculatedStats = this.calculatedStatsById.get(id);
|
|||
|
return calculatedStats ?
|
|||
|
calculatedStats.getCalculatedMetrics(originalMetricName) :
|
|||
|
[];
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Shows a `DOMHighResTimeStamp` as a human readable date time.
|
|||
|
// The metric must be a time value in milliseconds with Unix epoch as time
|
|||
|
// origin.
|
|||
|
class DateCalculator {
|
|||
|
constructor(metric) {
|
|||
|
this.metric = metric;
|
|||
|
}
|
|||
|
getCalculatedMetricName() {
|
|||
|
return '[' + this.metric + ']';
|
|||
|
}
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
const timestamp = currentReport.get(id)[this.metric];
|
|||
|
const date = new Date(timestamp);
|
|||
|
return date.toLocaleString();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Calculates the rate "delta accumulative / delta samples" and returns it. If
|
|||
|
// a rate cannot be calculated, such as the metric is missing in the current
|
|||
|
// or previous report, undefined is returned.
|
|||
|
class RateCalculator {
|
|||
|
constructor(
|
|||
|
accumulativeMetric, samplesMetric, modifier = CalculatorModifier.kNone) {
|
|||
|
this.accumulativeMetric = accumulativeMetric;
|
|||
|
this.samplesMetric = samplesMetric;
|
|||
|
this.modifier = modifier;
|
|||
|
}
|
|||
|
|
|||
|
getCalculatedMetricName() {
|
|||
|
const accumulativeMetric = this.modifier.bitrate ?
|
|||
|
this.accumulativeMetric + '_in_bits' :
|
|||
|
this.accumulativeMetric;
|
|||
|
if (this.samplesMetric === 'timestamp') {
|
|||
|
return '[' + accumulativeMetric + '/s]';
|
|||
|
}
|
|||
|
return '[' + accumulativeMetric + '/' + this.samplesMetric +
|
|||
|
this.modifier.postfix + ']';
|
|||
|
}
|
|||
|
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
return RateCalculator.calculateRate(
|
|||
|
id, previousReport, currentReport, this.accumulativeMetric,
|
|||
|
this.samplesMetric) *
|
|||
|
this.modifier.multiplier;
|
|||
|
}
|
|||
|
|
|||
|
static calculateRate(
|
|||
|
id, previousReport, currentReport, accumulativeMetric, samplesMetric) {
|
|||
|
if (!previousReport || !currentReport) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const previousStats = previousReport.get(id);
|
|||
|
const currentStats = currentReport.get(id);
|
|||
|
if (!previousStats || !currentStats) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const deltaTime = currentStats.timestamp - previousStats.timestamp;
|
|||
|
if (deltaTime <= 0) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
// Try to convert whatever the values are to numbers. This gets around the
|
|||
|
// fact that some types that are not supported by base::Value (e.g. uint32,
|
|||
|
// int64, uint64 and double) are passed as strings.
|
|||
|
const previousValue = Number(previousStats[accumulativeMetric]);
|
|||
|
const currentValue = Number(currentStats[accumulativeMetric]);
|
|||
|
if (typeof previousValue !== 'number' || typeof currentValue !== 'number') {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const previousSamples = Number(previousStats[samplesMetric]);
|
|||
|
const currentSamples = Number(currentStats[samplesMetric]);
|
|||
|
if (typeof previousSamples !== 'number' ||
|
|||
|
typeof currentSamples !== 'number') {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const deltaValue = currentValue - previousValue;
|
|||
|
const deltaSamples = currentSamples - previousSamples;
|
|||
|
return deltaValue / deltaSamples;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Looks up codec and payload type from a codecId reference, constructing an
|
|||
|
// informative string about which codec is used.
|
|||
|
class CodecCalculator {
|
|||
|
getCalculatedMetricName() {
|
|||
|
return '[codec]';
|
|||
|
}
|
|||
|
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
const targetStats = currentReport.get(id);
|
|||
|
const codecStats = currentReport.get(targetStats.codecId);
|
|||
|
if (!codecStats) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
// If mimeType is 'video/VP8' then codec is 'VP8'.
|
|||
|
const codec =
|
|||
|
codecStats.mimeType.substr(codecStats.mimeType.indexOf('/') + 1);
|
|||
|
|
|||
|
let fmtpLine = '';
|
|||
|
if (codecStats.sdpFmtpLine) {
|
|||
|
fmtpLine = ', ' + codecStats.sdpFmtpLine;
|
|||
|
}
|
|||
|
return codec + ' (' + codecStats.payloadType + fmtpLine + ')';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Calculates "RMS" audio level, which is the average audio level between the
|
|||
|
// previous and current report, in the interval [0,1]. Calculated per:
|
|||
|
// https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-totalaudioenergy
|
|||
|
class AudioLevelRmsCalculator {
|
|||
|
getCalculatedMetricName() {
|
|||
|
return '[Audio_Level_in_RMS]';
|
|||
|
}
|
|||
|
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
const averageAudioLevelSquared = RateCalculator.calculateRate(
|
|||
|
id, previousReport, currentReport, 'totalAudioEnergy',
|
|||
|
'totalSamplesDuration');
|
|||
|
return Math.sqrt(averageAudioLevelSquared);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Calculates "metricA - SUM(otherMetrics)", only looking at the current report.
|
|||
|
class DifferenceCalculator {
|
|||
|
constructor(metricA, ...otherMetrics) {
|
|||
|
this.metricA = metricA;
|
|||
|
this.otherMetrics = otherMetrics;
|
|||
|
}
|
|||
|
|
|||
|
getCalculatedMetricName() {
|
|||
|
return '[' + this.metricA + '-' + this.otherMetrics.join('-') + ']';
|
|||
|
}
|
|||
|
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
const currentStats = currentReport.get(id);
|
|||
|
return parseInt(currentStats[this.metricA], 10)
|
|||
|
- this.otherMetrics.map(metric => parseInt(currentStats[metric], 10))
|
|||
|
.reduce((a, b) => a + b, 0);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Calculates the standard deviation from a totalSquaredSum, totalSum, and
|
|||
|
// totalCount. If the standard deviation cannot be calculated, such as the
|
|||
|
// metric is missing in the current or previous report, undefined is returned.
|
|||
|
class StandardDeviationCalculator {
|
|||
|
constructor(totalSquaredSumMetric, totalSumMetric, totalCount, label) {
|
|||
|
this.totalSquaredSumMetric = totalSquaredSumMetric;
|
|||
|
this.totalSumMetric = totalSumMetric;
|
|||
|
this.totalCount = totalCount;
|
|||
|
this.label = label;
|
|||
|
}
|
|||
|
|
|||
|
getCalculatedMetricName() {
|
|||
|
return '[' + this.label + 'StDev_in_ms]';
|
|||
|
}
|
|||
|
|
|||
|
calculate(id, previousReport, currentReport) {
|
|||
|
return StandardDeviationCalculator.calculateStandardDeviation(
|
|||
|
id, previousReport, currentReport, this.totalSquaredSumMetric,
|
|||
|
this.totalSumMetric, this.totalCount);
|
|||
|
}
|
|||
|
|
|||
|
static calculateStandardDeviation(
|
|||
|
id, previousReport, currentReport, totalSquaredSumMetric, totalSumMetric,
|
|||
|
totalCount) {
|
|||
|
if (!previousReport || !currentReport) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const previousStats = previousReport.get(id);
|
|||
|
const currentStats = currentReport.get(id);
|
|||
|
if (!previousStats || !currentStats) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const deltaCount =
|
|||
|
Number(currentStats[totalCount]) - Number(previousStats[totalCount]);
|
|||
|
if (deltaCount <= 0) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
// Try to convert whatever the values are to numbers. This gets around the
|
|||
|
// fact that some types that are not supported by base::Value (e.g. uint32,
|
|||
|
// int64, uint64 and double) are passed as strings.
|
|||
|
const previousSquaredSumValue =
|
|||
|
Number(previousStats[totalSquaredSumMetric]);
|
|||
|
const currentSquaredSumValue = Number(currentStats[totalSquaredSumMetric]);
|
|||
|
if (typeof previousSquaredSumValue !== 'number' ||
|
|||
|
typeof currentSquaredSumValue !== 'number') {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
const previousSumValue = Number(previousStats[totalSumMetric]);
|
|||
|
const currentSumValue = Number(currentStats[totalSumMetric]);
|
|||
|
if (typeof previousSumValue !== 'number' ||
|
|||
|
typeof currentSumValue !== 'number') {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
|
|||
|
const deltaSquaredSum = currentSquaredSumValue - previousSquaredSumValue;
|
|||
|
const deltaSum = currentSumValue - previousSumValue;
|
|||
|
const variance =
|
|||
|
(deltaSquaredSum - Math.pow(deltaSum, 2) / deltaCount) / deltaCount;
|
|||
|
if (variance < 0) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
return 1000 * Math.sqrt(variance);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Keeps track of previous and current stats report and calculates all
|
|||
|
// calculated metrics.
|
|||
|
export class StatsRatesCalculator {
|
|||
|
constructor() {
|
|||
|
this.previousReport = null;
|
|||
|
this.currentReport = null;
|
|||
|
this.statsCalculators = [
|
|||
|
{
|
|||
|
type: 'data-channel',
|
|||
|
metricCalculators: {
|
|||
|
messagesSent: new RateCalculator('messagesSent', 'timestamp'),
|
|||
|
messagesReceived: new RateCalculator('messagesReceived', 'timestamp'),
|
|||
|
bytesSent: new RateCalculator(
|
|||
|
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
bytesReceived: new RateCalculator(
|
|||
|
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'media-source',
|
|||
|
metricCalculators: {
|
|||
|
totalAudioEnergy: new AudioLevelRmsCalculator(),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'outbound-rtp',
|
|||
|
metricCalculators: {
|
|||
|
bytesSent: new RateCalculator(
|
|||
|
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
headerBytesSent: new RateCalculator(
|
|||
|
'headerBytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
retransmittedBytesSent: new RateCalculator(
|
|||
|
'retransmittedBytesSent', 'timestamp',
|
|||
|
CalculatorModifier.kBytesToBits),
|
|||
|
packetsSent: new RateCalculator('packetsSent', 'timestamp'),
|
|||
|
retransmittedPacketsSent:
|
|||
|
new RateCalculator('retransmittedPacketsSent', 'timestamp'),
|
|||
|
totalPacketSendDelay: new RateCalculator(
|
|||
|
'totalPacketSendDelay', 'packetsSent',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
framesEncoded: new RateCalculator('framesEncoded', 'timestamp'),
|
|||
|
framesSent: new RateCalculator('framesSent', 'timestamp'),
|
|||
|
totalEncodedBytesTarget: new RateCalculator(
|
|||
|
'totalEncodedBytesTarget', 'timestamp',
|
|||
|
CalculatorModifier.kBytesToBits),
|
|||
|
totalEncodeTime: new RateCalculator(
|
|||
|
'totalEncodeTime', 'framesEncoded',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
qpSum: new RateCalculator('qpSum', 'framesEncoded'),
|
|||
|
codecId: new CodecCalculator(),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'inbound-rtp',
|
|||
|
metricCalculators: {
|
|||
|
bytesReceived: new RateCalculator(
|
|||
|
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
headerBytesReceived: new RateCalculator(
|
|||
|
'headerBytesReceived', 'timestamp',
|
|||
|
CalculatorModifier.kBytesToBits),
|
|||
|
retransmittedBytesReceived: new RateCalculator(
|
|||
|
'retransmittedBytesReceived', 'timestamp',
|
|||
|
CalculatorModifier.kBytesToBits),
|
|||
|
fecBytesReceived: new RateCalculator(
|
|||
|
'fecBytesReceived', 'timestamp',
|
|||
|
CalculatorModifier.kBytesToBits),
|
|||
|
packetsReceived: new RateCalculator('packetsReceived', 'timestamp'),
|
|||
|
packetsDiscarded: new RateCalculator('packetsDiscarded', 'timestamp'),
|
|||
|
retransmittedPacketsReceived:
|
|||
|
new RateCalculator('retransmittedPacketsReceived', 'timestamp'),
|
|||
|
fecPacketsReceived:
|
|||
|
new RateCalculator('fecPacketsReceived', 'timestamp'),
|
|||
|
fecPacketsDiscarded:
|
|||
|
new RateCalculator('fecPacketsDiscarded', 'timestamp'),
|
|||
|
framesReceived: [
|
|||
|
new RateCalculator('framesReceived', 'timestamp'),
|
|||
|
new DifferenceCalculator('framesReceived',
|
|||
|
'framesDecoded', 'framesDropped'),
|
|||
|
],
|
|||
|
framesDecoded: new RateCalculator('framesDecoded', 'timestamp'),
|
|||
|
keyFramesDecoded: new RateCalculator('keyFramesDecoded', 'timestamp'),
|
|||
|
totalDecodeTime: new RateCalculator(
|
|||
|
'totalDecodeTime', 'framesDecoded',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
totalInterFrameDelay: new RateCalculator(
|
|||
|
'totalInterFrameDelay', 'framesDecoded',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
totalSquaredInterFrameDelay: new StandardDeviationCalculator(
|
|||
|
'totalSquaredInterFrameDelay', 'totalInterFrameDelay',
|
|||
|
'framesDecoded', 'interFrameDelay'),
|
|||
|
totalSamplesReceived:
|
|||
|
new RateCalculator('totalSamplesReceived', 'timestamp'),
|
|||
|
concealedSamples: [
|
|||
|
new RateCalculator('concealedSamples', 'timestamp'),
|
|||
|
new RateCalculator('concealedSamples', 'totalSamplesReceived'),
|
|||
|
],
|
|||
|
silentConcealedSamples:
|
|||
|
new RateCalculator('silentConcealedSamples', 'timestamp'),
|
|||
|
insertedSamplesForDeceleration:
|
|||
|
new RateCalculator('insertedSamplesForDeceleration', 'timestamp'),
|
|||
|
removedSamplesForAcceleration:
|
|||
|
new RateCalculator('removedSamplesForAcceleration', 'timestamp'),
|
|||
|
qpSum: new RateCalculator('qpSum', 'framesDecoded'),
|
|||
|
codecId: new CodecCalculator(),
|
|||
|
totalAudioEnergy: new AudioLevelRmsCalculator(),
|
|||
|
jitterBufferDelay: new RateCalculator(
|
|||
|
'jitterBufferDelay', 'jitterBufferEmittedCount',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
jitterBufferTargetDelay: new RateCalculator(
|
|||
|
'jitterBufferTargetDelay', 'jitterBufferEmittedCount',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
jitterBufferMinimumDelay: new RateCalculator(
|
|||
|
'jitterBufferMinimumDelay', 'jitterBufferEmittedCount',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
lastPacketReceivedTimestamp: new DateCalculator(
|
|||
|
'lastPacketReceivedTimestamp'),
|
|||
|
estimatedPlayoutTimestamp: new DateCalculator(
|
|||
|
'estimatedPlayoutTimestamp'),
|
|||
|
totalProcessingDelay: new RateCalculator(
|
|||
|
'totalProcessingDelay', 'framesDecoded',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
totalAssemblyTime: new RateCalculator(
|
|||
|
'totalAssemblyTime', 'framesAssembledFromMultiplePackets',
|
|||
|
CalculatorModifier.kMillisecondsFromSeconds),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'remote-outbound-rtp',
|
|||
|
metricCalculators: {
|
|||
|
remoteTimestamp: new DateCalculator('remoteTimestamp'),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'transport',
|
|||
|
metricCalculators: {
|
|||
|
bytesSent: new RateCalculator(
|
|||
|
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
bytesReceived: new RateCalculator(
|
|||
|
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
packetsSent: new RateCalculator(
|
|||
|
'packetsSent', 'timestamp'),
|
|||
|
packetsReceived: new RateCalculator(
|
|||
|
'packetsReceived', 'timestamp'),
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: 'candidate-pair',
|
|||
|
metricCalculators: {
|
|||
|
bytesSent: new RateCalculator(
|
|||
|
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
bytesReceived: new RateCalculator(
|
|||
|
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
|||
|
packetsSent: new RateCalculator(
|
|||
|
'packetsSent', 'timestamp'),
|
|||
|
packetsReceived: new RateCalculator(
|
|||
|
'packetsReceived', 'timestamp'),
|
|||
|
totalRoundTripTime:
|
|||
|
new RateCalculator('totalRoundTripTime', 'responsesReceived'),
|
|||
|
lastPacketReceivedTimestamp: new DateCalculator(
|
|||
|
'lastPacketReceivedTimestamp'),
|
|||
|
lastPacketSentTimestamp: new DateCalculator(
|
|||
|
'lastPacketSentTimestamp'),
|
|||
|
},
|
|||
|
},
|
|||
|
];
|
|||
|
}
|
|||
|
|
|||
|
addStatsReport(report) {
|
|||
|
this.previousReport = this.currentReport;
|
|||
|
this.currentReport = report;
|
|||
|
this.updateCalculatedMetrics_();
|
|||
|
}
|
|||
|
|
|||
|
// Updates all "calculated metrics", which are metrics derived from standard
|
|||
|
// values, such as converting total counters (e.g. bytesSent) to rates (e.g.
|
|||
|
// bytesSent/s).
|
|||
|
updateCalculatedMetrics_() {
|
|||
|
this.statsCalculators.forEach(statsCalculator => {
|
|||
|
this.currentReport.getByType(statsCalculator.type).forEach(stats => {
|
|||
|
Object.keys(statsCalculator.metricCalculators)
|
|||
|
.forEach(originalMetric => {
|
|||
|
let metricCalculators =
|
|||
|
statsCalculator.metricCalculators[originalMetric];
|
|||
|
if (!Array.isArray(metricCalculators)) {
|
|||
|
metricCalculators = [metricCalculators];
|
|||
|
}
|
|||
|
metricCalculators.forEach(metricCalculator => {
|
|||
|
this.currentReport.addCalculatedMetric(
|
|||
|
stats.id, originalMetric,
|
|||
|
metricCalculator.getCalculatedMetricName(),
|
|||
|
metricCalculator.calculate(
|
|||
|
stats.id, this.previousReport, this.currentReport));
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
}
|