- patches for RTT / jitter / Network MOS calculation

This commit is contained in:
2026-05-04 14:29:21 +03:00
parent cfdf1a0c77
commit fd4f664164
4 changed files with 289 additions and 71 deletions
+186 -70
View File
@@ -1,4 +1,6 @@
#include <cmath>
#include <cctype>
#include <cstring>
#include <iostream>
#include "MT_Statistics.h"
@@ -6,60 +8,154 @@
using namespace MT;
namespace
{
// Per-codec impairment parameters (Ie, Bpl) from ITU-T G.113 / G.107.
// clockRate == 0 means "any".
struct MosCodecEntry { const char* mName; unsigned mClockRate; double mIe; double mBpl; };
constexpr MosCodecEntry kMosCodecTable[] = {
{ "PCMU", 8000, 0.0, 25.0 },
{ "PCMA", 8000, 0.0, 25.0 },
{ "G722", 8000, 13.0, 21.0 },
{ "G7221", 16000, 13.0, 21.0 },
{ "G7221", 32000, 13.0, 21.0 },
{ "G729", 8000, 11.0, 19.0 },
{ "G729A", 8000, 11.0, 19.0 },
{ "G729AB", 8000, 11.0, 19.0 },
{ "G723", 8000, 15.0, 16.0 },
{ "iLBC", 8000, 11.0, 18.0 },
{ "GSM", 8000, 20.0, 10.0 },
{ "AMR", 8000, 5.0, 10.0 },
{ "AMR-WB", 16000, 7.0, 10.0 },
{ "speex", 8000, 15.0, 20.0 },
{ "speex", 16000, 10.0, 20.0 },
{ "speex", 32000, 10.0, 20.0 },
{ "opus", 48000, 5.0, 25.0 },
// EVS — no published G.113 value. Using AMR-WB-family Bpl with a
// conservative Ie that matches typical commercial VQM tools for EVS
// Primary ~13.2 kbps WB.
{ "EVS", 16000, 5.0, 10.0 },
};
constexpr double kMosDefaultIe = 0.0;
constexpr double kMosDefaultBpl = 25.0;
bool iequals(const std::string& a, const char* b)
{
const size_t n = std::strlen(b);
if (a.size() != n) return false;
for (size_t i = 0; i < n; ++i)
if (std::tolower(static_cast<unsigned char>(a[i])) !=
std::tolower(static_cast<unsigned char>(b[i])))
return false;
return true;
}
void resolveMosCodecParams(const std::string& codecName, double& ie, double& bpl)
{
ie = kMosDefaultIe;
bpl = kMosDefaultBpl;
if (codecName.empty())
return;
// Map known codec-name aliases before looking up Ie/Bpl entries.
std::string lookup = codecName;
if (iequals(lookup, "GSM-06.10"))
lookup = "GSM";
for (const auto& e: kMosCodecTable)
if (iequals(lookup, e.mName))
{
ie = e.mIe;
bpl = e.mBpl;
return;
}
}
} // anonymous namespace
void JitterStatistics::process(jrtplib::RTPPacket* packet, int rate)
{
// Get current timestamp and receive time
uint32_t timestamp = packet->GetTimestamp();
jrtplib::RTPTime receiveTime = packet->GetReceiveTime();
// RFC 3550 §A.8 jitter. Two guards:
//
// 1. Update only when the new packet is exactly one sequence number
// after the previous in-sequence packet. Skipping this check across
// packet-loss gaps inflates jitter; skipping out-of-order packets
// entirely (the previous behaviour) under-reports it.
// 2. Ignore the first few in-sequence samples while transit time
// settles after call setup.
constexpr uint32_t kIgnoreFirstPackets = 5;
const uint32_t timestamp = packet->GetTimestamp();
const uint32_t extSeqno = packet->GetExtendedSequenceNumber();
const jrtplib::RTPTime receiveTime = packet->GetReceiveTime();
// First packet: just stash state.
if (!mLastJitter)
{
// First packet
mReceiveTime = receiveTime;
mReceiveTime = receiveTime;
mReceiveTimestamp = timestamp;
mLastJitter = 0.0;
mLastExtSeqno = extSeqno;
mLastJitter = 0.0;
mPacketsProcessed = 1;
return;
}
else
// RFC 3550 §A.8: only adjacent packets contribute to jitter.
// Out-of-order, duplicate, and post-loss packets are skipped silently —
// but state must still advance so the *next* in-sequence pair works.
const bool adjacent = mLastExtSeqno && (extSeqno == mLastExtSeqno.value() + 1);
if (!adjacent)
{
// It is in units
int64_t receiveDelta = int64_t(receiveTime.GetDouble() * rate) - int64_t(mReceiveTime.GetDouble() * rate);
// Check if packets are ordered ok
if (timestamp <= mReceiveTimestamp)
return;
// Find differences in timestamp
int64_t timestampDelta = timestamp - mReceiveTimestamp;
if (!timestampDelta)
// Skip current packet silently. Most probably it is error in RTP stream like duplicated packet.
return;
// Find delta in units
int64_t delta = receiveDelta - timestampDelta;
// Update max delta in milliseconds
float delta_in_seconds = float(fabs(double(delta) / rate));
if (delta_in_seconds > mMaxDelta)
mMaxDelta = delta_in_seconds;
// Update jitter value in units
mLastJitter = mLastJitter.value() + (fabs(double(delta)) - mLastJitter.value()) / 16.0;
/*printf("PacketNo: %d, current delta in ms: %f, jitter in ms: %f\n",
(int)packet->GetSequenceNumber(),
delta_in_ms,
float(mLastJitter.value() / (rate / 1000)));*/
// Save last values
mReceiveTime = receiveTime;
mReceiveTimestamp = timestamp;
// And mJitter are in milliseconds again
float jitter_s = mLastJitter.value() / (float(rate));
// std::cout << "Jitter (in seconds): " << std::dec << jitter_s << std::endl;
mJitter.process(jitter_s);
// Reset the transit reference if a discontinuity (loss / reorder)
// happened, restarting from the latest known good packet.
if (mLastExtSeqno && extSeqno > mLastExtSeqno.value())
{
mReceiveTime = receiveTime;
mReceiveTimestamp = timestamp;
mLastExtSeqno = extSeqno;
}
return;
}
// RTP FAQ: also skip when timestamp is unchanged (multi-packet frame, dup).
if (timestamp == mReceiveTimestamp)
{
mLastExtSeqno = extSeqno;
return;
}
// Wrap-safe signed delta on the 32-bit RTP timestamp:
// transit = arrival - rtp_ts; d = transit - prev_transit (signed 32-bit).
const int32_t timestampDelta = static_cast<int32_t>(timestamp - mReceiveTimestamp);
const int64_t receiveDelta =
static_cast<int64_t>(receiveTime.GetDouble() * rate) -
static_cast<int64_t>(mReceiveTime.GetDouble() * rate);
const int64_t delta = receiveDelta - timestampDelta;
// Save state for the next pair regardless of warmup.
mReceiveTime = receiveTime;
mReceiveTimestamp = timestamp;
mLastExtSeqno = extSeqno;
++mPacketsProcessed;
// Skip the first N in-sequence samples while transit time settles.
if (mPacketsProcessed <= kIgnoreFirstPackets)
return;
const float deltaSec = static_cast<float>(std::fabs(static_cast<double>(delta) / rate));
if (deltaSec > mMaxDelta)
mMaxDelta = deltaSec;
// J = J + (|D| - J) / 16
mLastJitter = mLastJitter.value() +
(std::fabs(static_cast<double>(delta)) - mLastJitter.value()) / 16.0;
mJitter.process(mLastJitter.value() / static_cast<float>(rate));
}
@@ -94,11 +190,9 @@ void Statistics::calculateBurstr(double* burstr, double* lossr) const
if (mReceivedRtp > 0 && bursts > 0)
{
*burstr = (double)((double)lost / (double)bursts) / (double)(1.0 / (1.0 - (double)lost / (double)mReceivedRtp));
if (*burstr < 0)
*burstr = -*burstr;
else if (*burstr < 1)
*burstr = 1;
*burstr = ((double)lost / (double)bursts) * (1.0 - (double)lost / (double)mReceivedRtp);
if (*burstr < 1.0)
*burstr = 1.0;
}
else
*burstr = 0;
@@ -111,34 +205,56 @@ void Statistics::calculateBurstr(double* burstr, double* lossr) const
double Statistics::calculateMos(double maximalMos) const
{
// calculate lossrate and burst rate
double burstr = 0, lossr = 0;
calculateBurstr(&burstr, &lossr);
double r = 0.0;
double bpl = 8.47627; //mos = -4.23836 + 0.29873 * r - 0.00416744 * r * r + 0.0000209855 * r * r * r;
double mos = 0.0;
// Network MOS via the simplified ITU-T G.107 E-Model:
//
// d_oneway = rtt/2 + jitter + jb_delay (ms)
// Id = 0.024*d + 0.11*max(0, d - 177.3)
// Ie_eff = Ie + (95 - Ie) * Ppl / (Ppl + Bpl) (BurstR=1)
// R = 93.2 - Id - Ie_eff (clamped to [0,100])
// MOS = 1 + 0.035*R + 7e-6*R*(R-60)*(100-R) (clamped to [1, maximalMos])
//
// Ie/Bpl are looked up from a per-codec table; safe defaults are used
// when the codec is unknown.
if (mReceivedRtp < 10)
return 0.0;
if (lossr == 0.0 || burstr == 0.0)
{
return maximalMos;
}
// Loss percent is computed as lost / (lost + received).
const uint64_t expected = static_cast<uint64_t>(mReceivedRtp) +
static_cast<uint64_t>(mPacketLoss);
const double Ppl = expected > 0
? static_cast<double>(mPacketLoss) * 100.0 / static_cast<double>(expected)
: 0.0;
if (lossr > 0.5)
return 1;
double Ie = kMosDefaultIe, Bpl = kMosDefaultBpl;
resolveMosCodecParams(mCodecName, Ie, Bpl);
if (Bpl <= 0.0)
Bpl = 1.0;
bpl = 17.2647;
r = 93.2062077233 - 95.0 * (lossr * 100 / (lossr * 100 / burstr + bpl));
mos = 2.06405 + 0.031738 * r - 0.000356641 * r * r + 2.93143 * pow(10, -6) * r * r * r;
if (mos < 1)
return 1;
// mRttDelay and mJitter are stored in seconds. jb_delay is unknown at
// this layer, so it is treated as zero.
const double rttMs = static_cast<double>(mRttDelay.average()) * 1000.0;
const double jitterMs = static_cast<double>(mJitter) * 1000.0;
const double d = rttMs / 2.0 + jitterMs;
if (mos > maximalMos)
return maximalMos;
double Id = 0.024 * d;
if (d > 177.3)
Id += 0.11 * (d - 177.3);
const double Ie_eff = Ie + (95.0 - Ie) * Ppl / (Ppl + Bpl);
double R = 93.2 - Id - Ie_eff;
if (R < 0.0) R = 0.0;
if (R > 100.0) R = 100.0;
double mos;
if (R == 0.0)
mos = 1.0;
else
mos = 1.0 + 0.035 * R + 7e-6 * R * (R - 60.0) * (100.0 - R);
if (mos < 1.0) mos = 1.0;
if (mos > maximalMos) mos = maximalMos;
return mos;
}