Skip to content

Commit

Permalink
Simplify and standardize stats calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
streamer45 committed Oct 10, 2024
1 parent 8d2b13b commit e05911d
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 66 deletions.
41 changes: 11 additions & 30 deletions lib/rtc_monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
import { EventEmitter } from 'events';
import { newRTCLocalInboundStats, newRTCLocalOutboundStats, newRTCRemoteInboundStats, newRTCRemoteOutboundStats, newRTCCandidatePairStats } from './rtc_stats';
import { newRTCLocalInboundStats, newRTCLocalOutboundStats, newRTCRemoteInboundStats, newRTCRemoteOutboundStats } from './rtc_stats';
export const mosThreshold = 3.5;
export class RTCMonitor extends EventEmitter {
constructor(cfg) {
Expand Down Expand Up @@ -79,7 +79,7 @@ export class RTCMonitor extends EventEmitter {
}
if (totalLocalStats > 0) {
stats.avgTime = totalTime / totalLocalStats;
stats.avgJitter = (totalJitter / totalLocalStats) * 1000;
stats.avgJitter = totalJitter / totalLocalStats;
}
if (totalPacketsReceived > 0) {
stats.avgLossRate = totalPacketsLost / totalPacketsReceived;
Expand All @@ -90,7 +90,6 @@ export class RTCMonitor extends EventEmitter {
var _a;
const stats = {};
let totalTime = 0;
let totalRTT = 0;
let totalRemoteJitter = 0;
let totalRemoteStats = 0;
let totalLossRate = 0;
Expand All @@ -104,14 +103,12 @@ export class RTCMonitor extends EventEmitter {
const tsDiff = stat.timestamp - this.stats.lastRemoteIn[ssrc].timestamp;
totalTime += tsDiff;
totalRemoteJitter += stat.jitter;
totalRTT += stat.roundTripTime;
totalLossRate += stat.fractionLost;
totalRemoteStats++;
}
if (totalRemoteStats > 0) {
stats.avgTime = totalTime / totalRemoteStats;
stats.avgJitter = (totalRemoteJitter / totalRemoteStats) * 1000;
stats.avgLatency = (totalRTT / totalRemoteStats) * (1000 / 2);
stats.avgJitter = totalRemoteJitter / totalRemoteStats;
stats.avgLossRate = totalLossRate / totalRemoteStats;
}
return stats;
Expand All @@ -121,17 +118,10 @@ export class RTCMonitor extends EventEmitter {
const localOut = {};
const remoteIn = {};
const remoteOut = {};
let candidate;
reports.forEach((report) => {
// Collect necessary stats to make further calculations:
// - candidate-pair: transport level metrics.
// - inbound-rtp: metrics for incoming RTP media streams.
// - remote-inbound-rtp: metrics for outgoing RTP media streams as received by the remote endpoint.
if (report.type === 'candidate-pair' && report.nominated) {
if (!candidate || (report.priority && candidate.priority && report.priority > candidate.priority)) {
candidate = newRTCCandidatePairStats(report, reports);
}
}
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
localIn[report.ssrc] = newRTCLocalInboundStats(report);
}
Expand All @@ -145,16 +135,9 @@ export class RTCMonitor extends EventEmitter {
remoteOut[report.ssrc] = newRTCRemoteOutboundStats(report);
}
});
if (!candidate) {
this.logger.logDebug('RTCMonitor: no valid candidate was found');
return;
}
// Step 1: get transport latency from the in-use candidate pair stats, if present.
let transportLatency;
// currentRoundTripTime could be missing in the original report (e.g. on Firefox) and implicitly coverted to NaN.
if (!isNaN(candidate.currentRoundTripTime)) {
transportLatency = (candidate.currentRoundTripTime * 1000) / 2;
}
// Step 1: get transport round-trip time from the peer.
// This is calculated through ping/pong messages on the data channel.
const transportRTT = this.peer.getRTT();
// Step 2: if receiving any stream, calculate average jitter and loss rate using local stats.
const localInStats = this.getLocalInQualityStats(localIn, remoteOut);
// Step 3: if sending any stream, calculate average latency, jitter and
Expand All @@ -165,9 +148,6 @@ export class RTCMonitor extends EventEmitter {
this.stats.lastLocalOut = Object.assign({}, localOut);
this.stats.lastRemoteIn = Object.assign({}, remoteIn);
this.stats.lastRemoteOut = Object.assign({}, remoteOut);
if (typeof transportLatency === 'undefined' && typeof remoteInStats.avgLatency === 'undefined') {
transportLatency = this.peer.getRTT() / 2;
}
if (typeof localInStats.avgJitter === 'undefined' && typeof remoteInStats.avgJitter === 'undefined') {
this.logger.logDebug('RTCMonitor: jitter could not be calculated');
return;
Expand All @@ -178,15 +158,16 @@ export class RTCMonitor extends EventEmitter {
}
const jitter = Math.max(localInStats.avgJitter || 0, remoteInStats.avgJitter || 0);
const lossRate = Math.max(localInStats.avgLossRate || 0, remoteInStats.avgLossRate || 0);
const latency = transportLatency !== null && transportLatency !== void 0 ? transportLatency : remoteInStats.avgLatency;
const latency = transportRTT / 2; // approximating one-way latency as RTT/2
// Step 5 (or the magic step): calculate MOS (Mean Opinion Score)
const mos = this.calculateMOS(latency, jitter, lossRate);
// Latency and jitter values are expected to be in ms rather than seconds.
const mos = this.calculateMOS(latency * 1000, jitter * 1000, lossRate);
this.emit('mos', mos);
this.peer.handleMetrics(lossRate, jitter / 1000);
this.peer.handleMetrics(lossRate, jitter);
this.logger.logDebug(`RTCMonitor: MOS --> ${mos}`);
}
calculateMOS(latency, jitter, lossRate) {
this.logger.logDebug(`RTCMonitor: MOS inputs --> latency: ${latency} jitter: ${jitter} loss: ${lossRate}`);
this.logger.logDebug(`RTCMonitor: MOS inputs --> latency: ${latency.toFixed(1)}ms jitter: ${jitter.toFixed(1)}ms loss: ${(lossRate * 100).toFixed(2)}%`);
let R = 0;
const effectiveLatency = latency + (2 * jitter) + 10.0;
if (effectiveLatency < 160) {
Expand Down
48 changes: 12 additions & 36 deletions src/rtc_monitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {EventEmitter} from 'events';

import {Logger, RTCMonitorConfig, RTCLocalInboundStats, RTCRemoteInboundStats, RTCRemoteOutboundStats, RTCLocalOutboundStats, RTCCandidatePairStats} from './types';
import {newRTCLocalInboundStats, newRTCLocalOutboundStats, newRTCRemoteInboundStats, newRTCRemoteOutboundStats, newRTCCandidatePairStats} from './rtc_stats';
import {Logger, RTCMonitorConfig, RTCLocalInboundStats, RTCRemoteInboundStats, RTCRemoteOutboundStats, RTCLocalOutboundStats} from './types';
import {newRTCLocalInboundStats, newRTCLocalOutboundStats, newRTCRemoteInboundStats, newRTCRemoteOutboundStats} from './rtc_stats';
import {RTCPeer} from './rtc_peer';

export const mosThreshold = 3.5;
Expand Down Expand Up @@ -33,7 +33,6 @@ type CallQualityStats = {
avgTime?: number,
avgLossRate?: number,
avgJitter?: number,
avgLatency?: number,
};

export class RTCMonitor extends EventEmitter {
Expand Down Expand Up @@ -123,7 +122,7 @@ export class RTCMonitor extends EventEmitter {

if (totalLocalStats > 0) {
stats.avgTime = totalTime / totalLocalStats;
stats.avgJitter = (totalJitter / totalLocalStats) * 1000;
stats.avgJitter = totalJitter / totalLocalStats;
}

if (totalPacketsReceived > 0) {
Expand All @@ -137,7 +136,6 @@ export class RTCMonitor extends EventEmitter {
const stats: CallQualityStats = {};

let totalTime = 0;
let totalRTT = 0;
let totalRemoteJitter = 0;
let totalRemoteStats = 0;
let totalLossRate = 0;
Expand All @@ -153,15 +151,13 @@ export class RTCMonitor extends EventEmitter {
const tsDiff = stat.timestamp - this.stats.lastRemoteIn[ssrc].timestamp;
totalTime += tsDiff;
totalRemoteJitter += stat.jitter;
totalRTT += stat.roundTripTime;
totalLossRate += stat.fractionLost;
totalRemoteStats++;
}

if (totalRemoteStats > 0) {
stats.avgTime = totalTime / totalRemoteStats;
stats.avgJitter = (totalRemoteJitter / totalRemoteStats) * 1000;
stats.avgLatency = (totalRTT / totalRemoteStats) * (1000 / 2);
stats.avgJitter = totalRemoteJitter / totalRemoteStats;
stats.avgLossRate = totalLossRate / totalRemoteStats;
}

Expand All @@ -173,19 +169,11 @@ export class RTCMonitor extends EventEmitter {
const localOut: LocalOutboundStatsMap = {};
const remoteIn: RemoteInboundStatsMap = {};
const remoteOut: RemoteOutboundStatsMap = {};
let candidate: RTCCandidatePairStats | undefined;
reports.forEach((report: any) => {
// Collect necessary stats to make further calculations:
// - candidate-pair: transport level metrics.
// - inbound-rtp: metrics for incoming RTP media streams.
// - remote-inbound-rtp: metrics for outgoing RTP media streams as received by the remote endpoint.

if (report.type === 'candidate-pair' && report.nominated) {
if (!candidate || (report.priority && candidate.priority && report.priority > candidate.priority)) {
candidate = newRTCCandidatePairStats(report, reports);
}
}

if (report.type === 'inbound-rtp' && report.kind === 'audio') {
localIn[report.ssrc] = newRTCLocalInboundStats(report);
}
Expand All @@ -203,18 +191,9 @@ export class RTCMonitor extends EventEmitter {
}
});

if (!candidate) {
this.logger.logDebug('RTCMonitor: no valid candidate was found');
return;
}

// Step 1: get transport latency from the in-use candidate pair stats, if present.
let transportLatency;

// currentRoundTripTime could be missing in the original report (e.g. on Firefox) and implicitly coverted to NaN.
if (!isNaN(candidate.currentRoundTripTime)) {
transportLatency = (candidate.currentRoundTripTime * 1000) / 2;
}
// Step 1: get transport round-trip time from the peer.
// This is calculated through ping/pong messages on the data channel.
const transportRTT = this.peer.getRTT();

// Step 2: if receiving any stream, calculate average jitter and loss rate using local stats.
const localInStats = this.getLocalInQualityStats(localIn, remoteOut);
Expand All @@ -237,10 +216,6 @@ export class RTCMonitor extends EventEmitter {
...remoteOut,
};

if (typeof transportLatency === 'undefined' && typeof remoteInStats.avgLatency === 'undefined') {
transportLatency = this.peer.getRTT() / 2;
}

if (typeof localInStats.avgJitter === 'undefined' && typeof remoteInStats.avgJitter === 'undefined') {
this.logger.logDebug('RTCMonitor: jitter could not be calculated');
return;
Expand All @@ -253,17 +228,18 @@ export class RTCMonitor extends EventEmitter {

const jitter = Math.max(localInStats.avgJitter || 0, remoteInStats.avgJitter || 0);
const lossRate = Math.max(localInStats.avgLossRate || 0, remoteInStats.avgLossRate || 0);
const latency = transportLatency ?? remoteInStats.avgLatency;
const latency = transportRTT / 2; // approximating one-way latency as RTT/2

// Step 5 (or the magic step): calculate MOS (Mean Opinion Score)
const mos = this.calculateMOS(latency!, jitter, lossRate);
// Latency and jitter values are expected to be in ms rather than seconds.
const mos = this.calculateMOS(latency * 1000, jitter * 1000, lossRate);
this.emit('mos', mos);
this.peer.handleMetrics(lossRate, jitter / 1000);
this.peer.handleMetrics(lossRate, jitter);
this.logger.logDebug(`RTCMonitor: MOS --> ${mos}`);
}

private calculateMOS(latency: number, jitter: number, lossRate: number) {
this.logger.logDebug(`RTCMonitor: MOS inputs --> latency: ${latency} jitter: ${jitter} loss: ${lossRate}`);
this.logger.logDebug(`RTCMonitor: MOS inputs --> latency: ${latency.toFixed(1)}ms jitter: ${jitter.toFixed(1)}ms loss: ${(lossRate * 100).toFixed(2)}%`);

let R = 0;
const effectiveLatency = latency + (2 * jitter) + 10.0;
Expand Down

0 comments on commit e05911d

Please sign in to comment.