live555源码分析——client RTCP处理(RR报告构造详解)
2026/6/1 6:43:10 网站建设 项目流程

简介

本文详细分析live555客户端RTCP处理机制,重点探讨RR(Receiver Report)报告的构造过程。


核心类分析

在分析rtcp的具体处理之前,我们先对与rtcp相关的类进行分析。

RTPSource - RTP数据源 (抽象基类)

继承关系:FrameSource -> RTPSource

核心职责:代表一个RTP数据流的接收端,封装了网络接口和接收状态。

关键成员:

分类成员说明
网络层fRTPInterface底层 RTP 收发接口,支持 UDP/TCP
加密fCryptoSRTP 加密上下文(可选)
当前包信息fCurPacketRTPSeqNum/fCurPacketRTPTimestamp/fCurPacketMarkerBit最近一个 RTP 包的元数据
标识fSSRC本端 SSRC;fLastReceivedSSRC是对端的
统计fReceptionStatsDB持有一个 RTPReceptionStatsDB 实例
控制fEnableRTCPReports是否允许发送 RTCP RR 报告

RTPSource在哪被使用?

RTPSource是MediaSubsession的成员。MediaSubsession::createSourceObjects()用于创建各种类型的RTPSource,对于H264来说就是H264VideoRTPSource

继承关系:RTPSource -> MultiFramedRTPSource -> H264VideoRTPSource

调用关系:setupNextSubsession() -> MediaSubsession::initiate() -> MediaSubsession::createSourceObjects()


RTPReceptionStatsDB — 接收统计数据库

核心职责:管理多个发送端(SSRC)的统计记录,以哈希表为底层结构。

RTPReceptionStatsDB └── HashTable<SSRC → RTPReceptionStats*>

什么情况下有多个发送端(多源)?

典型的场景是多播会议:

发送方A(SSRC=0x1111)──┐ 发送方B(SSRC=0x2222)──┼──→ 组播组 ──→ 你的接收端 发送方C(SSRC=0x3333)──┘

作为接收方,同时收到来自A、B、C三个人的RTP包,你需要分别统计每个人的丢包率、抖动等质量指标,然后在RTCP RR报告里分别汇报给他们。

普通的点对点单播(比如你看一个视频直播),通常也只有一个 SSRC,数据库里就只有一条记录。多源管理在这种情况下是退化的,但代码结构保持一致。

关键方法:

方法触发时机作用
noteIncomingPacket()收到 RTP 包更新序列号、时间戳、抖动、呈现时间
noteIncomingSR()收到 RTCP SR 包同步 NTP/RTP 时间戳,用于音视频同步
removeRecord()收到 RTCP BYE移除某个发送端的记录
reset()每次生成 RTCP RR 前重置周期性统计(如丢包率)

迭代器设计:内嵌 Iterator 类,遍历所有 SSRC 的统计,可选择是否包含非活跃源,供生成 RTCP RR 报告时使用。


RTPReceptionStats — 单个发送端的接收统计

核心职责:单一 SSRC的 RTP 流做完整的接收质量统计,数据直接用于生成 RTCP RR(接收报告)。

统计字段分类:

包计数 ├── fTotNumPacketsReceived 总收包数 ├── fNumPacketsReceivedSinceLastReset 周期收包数 └── fTotBytesReceived_hi/lo 总字节数(64位拆分存储) 序列号追踪(用于计算丢包) ├── fBaseExtSeqNumReceived 起始扩展序列号 ├── fLastResetExtSeqNumReceived 上次重置时的序列号 └── fHighestExtSeqNumReceived 已见最大扩展序列号 抖动计算(RFC3550算法) ├── fJitter 当前抖动估计值 ├── fLastTransit 上一包的传输延迟 └── fPreviousPacketRTPTimestamp 上一包的 RTP 时间戳 时钟同步(依赖 RTCP SR) ├── fLastReceivedSR_NTPmsw/lsw 最近 SR 的 NTP 时间戳 ├── fSyncTimestamp/fSyncTime 同步基准点 └── fHasBeenSynchronized 是否已完成 NTP 同步 包间隔统计 ├── fMinInterPacketGapUS ├── fMaxInterPacketGapUS └── fTotalInterPacketGaps

RTPSource、RTPReceptionStatsDB、RTPReceptionStats、RTCP模块关系:

RTPSource 管网络与生命周期,RTPReceptionStatsDB 管多源索引,RTPReceptionStats 管单源质量统计。

RTPSource │ 收到 RTP 包 ▼ RTPReceptionStatsDB.noteIncomingPacket(SSRC,seqNum,...)│ 按 SSRC 查找或创建 ▼ RTPReceptionStats.noteIncomingPacket(...)│ 更新序列号、计算抖动、推导呈现时间 ▼ (RTCP 模块)读取统计 → 生成并发送 RR 报告

SDESItem — 源描述条目

职责:封装一个 RTCP 源描述字段,比如发送方的名字、邮件、工具名等。

内存布局非常简单,直接对应网络包格式:

┌─────────┬──────────┬──────────────────────┐ │tag(1B)length(1B)│ value(最多255字节) │ └─────────┴──────────┴──────────────────────┘

其中 tag 的取值就是文件末尾定义的那些常量,最重要的是RTCP_SDES_CNAME(规范名),它是区分不同发送方的唯一标识,音视频同步依赖它。


RTCPInstance — RTCP 控制实例

继承:Medium

核心职责:负责定时发送 RTCP 报告和接收并处理对端的 RTCP 报告,是 RTP 会话的控制通道。

关键成员:

成员类型说明
fSinkRTPSink*关联的发送端,有它则发 SR 报告
fSourceRTPSource*关联的接收端,有它则发 RR 报告
fRTCPInterfaceRTPInterface底层网络收发接口
fCNAMESDESItem本端的规范名标识
fKnownMembersRTCPMemberDatabase*已知会话成员的数据库
fTotSessionBWunsigned会话总带宽(kbps),用于控制 RTCP 自身占用的流量
fInBuf/fOutBuf缓冲区收包和发包的缓冲

定时器相关成员:

成员说明
fAveRTCPSizeRTCP 包的平均大小,动态估算
fPrevReportTime/fNextReportTime上次/下次发报时间
fIsInitial是否处于初始阶段(初始阶段发送间隔更短)

RTCP 不是随时想发就发的,RFC 3550 规定它最多只能占用会话带宽的5%,所以发送间隔需要根据带宽动态计算。

回调处理函数:

RTCPInstance 收到不同类型的 RTCP 包时,会触发对应的回调:

回调触发时机
fSRHandlerTask收到 SR(发送端报告)时
fRRHandlerTask收到 RR(接收端报告)时
fByeHandlerTask收到 BYE(离开通知)时
fAppHandlerTask收到 APP(自定义应用包)时
fSpecificRRHandlerTable收到来自特定地址的 RR 时

其中 BYE 的回调是一次性的,触发后需要重新注册;SR/RR 的回调则每次收到都会触发。

与RTPSource关系:

RTCPInstance │ ├── 读取 fSink 的发送统计 ──→ 构造 SR 报告发出去 │ ├── 读取 fSource 的接收统计 ──→ 构造 RR 报告发出去 │ └── RTPReceptionStatsDB │ └── RTPReceptionStats(每个 SSRC 一份) │ └── 收到对端 SR 后 ──→ 写回 RTPReceptionStatsDB (更新 NTP 时间戳,完成音视频同步)

简单说:RTPSource 负责收数据,RTCPInstance 负责汇报收得怎么样,两者通过 RTPReceptionStatsDB 共享统计数据。

RTCPInstance在哪被使用?

RTCPInstance是MediaSubsession的成员。RTCPInstance在MediaSubsession::initiate()中被创建。

调用关系:setupNextSubsession() -> MediaSubsession::initiate()


RR处理

完整调用链

1.RTCPInstance::RTCPInstance()调用onExpire(this)发送第一个 RTCP 报告2.onExpire(RTCPInstance instance)调用 instance->onExpire1()3.onExpire1()调用OnExpire(this,...)-这是 rtcp_from_spec.c 中的算法函数4.OnExpire()(在 rtcp_from_spec.c 中),调用SendRTCPReport()发送RR, 然后根据 RTCP 定时算法,调用Schedule(tn,e)Schedule(t+tc,e)5.Schedule(doublenextTime,event e)C 接口函数,调用 instance->schedule(nextTime)6.schedule(doublenextTime)最终执行:设置定时任务envir().taskScheduler().scheduleDelayedTask()

RR构造

RR构造在RTCPInstance::enqueueReportBlock()中,下面依次分析各字段。

SSRC

含义

发出此 RR 包的参与者的同步源标识符。由于一个 RR 包可以包含多个报告块,发送端通过这个 ID 知道哪一段统计数据对应哪一个媒体流。

如何得到

RTCPInstance在收到发送端的SR后,会为每一个发送端创建一个RTPReceptionStats,SR中的SSRC作为RTPReceptionStats的唯一标识符,相关代码见:RTPReceptionStatsDB::noteIncomingSR()。构造RR时会依次从RTPReceptionStatsDB中取RTPReceptionStats,即会为每个发送端构造一个RR,RR中的SSRC就是RTPReceptionStats的唯一标识符SSRC。


totNumLost – 总丢包数

含义

自开始接收以来,该源丢失数据包的总数。

计算方法

总丢包数 = 期望接收总数 - 实际接收总数
  • 期望接收总数:基于最大的序列号(考虑回绕)减去起始序列号。
  • 实际接收总数:接收端实际收到的不重复数据包数量。

注意:它是 24 位有符号整数,如果收到的包比期望的还多(例如由于重复包),该值可能会是负数。

什么是回绕?

在 RTP 协议中,序列号(Sequence Number)是一个16 位(16-bit)的字段。这意味着它的取值范围是从 0 到 65535。

所谓的"考虑回绕(Wrap-around)“,是指当序列号达到最大值 65535 后,下一个包的序列号会重新回到 0。在计算丢包率或期望收到的包总数时,我们不能简单地用"最大减最小”,而必须把这些"圈数"算进去。

为什么要考虑回绕?

想象你在操场跑道上计数:

  • 起点序列号是 65530。
  • 你跑了一会儿,看到当前的序列号是 5。

如果你直接做减法:5 - 65530 = -65525。这显然不对,你不可能收到了负数个包。

实际上,序列号经历了:65530 ➤ 65531 … ➤ 65535 0 1 5。你实际跑了 12 个位置。这就是"回绕"。

扩展序列号(Extended Sequence Number)

为了解决这个问题,接收端会在内存中维护一个 32 位或 64 位的"扩展序列号"。

它由两部分组成:

  • 周期计数器 (Cycles):记录序列号从 65535 回到 0 的次数。
  • 当前序列号 (Seq):也就是 RTP 包头里那个 16 位的值。

计算公式:Extended_Seq = (Cycles * 65536) + Seq

如何判断发生了回绕?

接收端会对比"当前收到的序列号(S_new)“和"之前收到的最大序列号(S_max)”:

  • 正常递增:S_new > S_max(且差值较小),直接更新S_max。
  • 发生回绕:如果 S_new 突然变得很小(例如 S_max=65530 而 S_new=10),接收端会判断发生了一次翻转,将Cycles加 1。
  • 乱序/重传:如果S_new比 S_max 小很多,但又不符合回绕逻辑,则判定为迟到的包或乱序包。

在 RTCP RR 包中的体现

在 Receiver Report 中,Extended Highest Sequence Number Received字段正好就是32 位

  • 高 16 位:存储回绕次数(Cycles)。
  • 低 16 位:存储收到的最高原生序列号(Seq)。

通过这种方式,发送端只需要简单地用这个 32 位的值减去初始序列号,就能精准算出从通话开始到现在,一共应该收到多少个包,从而算出最准确的累计丢包率。

代码分析

核心代码如下:

unsignedhighestExtSeqNumReceived=stats->highestExtSeqNumReceived();// 最高扩展序列号// 期望收到的包数 = 最高扩展序列号 - 起始扩展序列号unsignedtotNumExpected=highestExtSeqNumReceived-stats->baseExtSeqNumReceived();// 丢失包数 = 期望收到 - 实际收到// totNumLost 为负数是可能发生的,因为重传或乱序可能导致实际收到的包比期望的还多。inttotNumLost=totNumExpected-stats->totNumPacketsReceived();// 截断到24位。在RTCP RR中,累计丢包数字段只有24位,所以必须截断到24位。if(totNumLost>0x007FFFFF){totNumLost=0x007FFFFF;// 即 8388607,24位有符号正数最大值}elseif(totNumLost<0){if(totNumLost<-0x00800000)totNumLost=0x00800000;totNumLost&=0x00FFFFFF;}

loss fraction – 瞬时丢包率

含义

自上一个 RR 包发送以来,该源数据包的瞬时丢包率,也可成为周期丢包率,周期是指上次发送RR到本次发送RR这段时间。

计算方法

  1. 计算期望接收到的包数N_exp = {本次最高的序列号} - {上次最高的序列号}
  2. 计算实际丢失的包数N_lost = {本次累计丢包数} - {上次累计丢包数}
  3. 计算比例:Fraction = (N_lost / N_exp)* 256

注意:如果结果为负(比如收到重复包),则取 0。该值最大为 255(代表 100% 丢包)。

代码分析

// 本周期期待收包数 = 当前最高扩展序列号 - 上次最高扩展序列号unsignednumExpectedSinceLastReset=highestExtSeqNumReceived-stats->lastResetExtSeqNumReceived();// 本周期丢包数 = 本周期期待收包数 - 本周期实际收包数intnumLostSinceLastReset=numExpectedSinceLastReset-stats->numPacketsReceivedSinceLastReset();unsignedcharlossFraction;if(numExpectedSinceLastReset==0||numLostSinceLastReset<0){// 直接置0的两种情况// 这个周期内没有期望收到任何包(可能刚重置)// 丢包数为负(收到的比期望的多,说明有重传)lossFraction=0;}else{lossFraction=(unsignedchar)((numLostSinceLastReset<<8)/numExpectedSinceLastReset);}// 解释// lossFraction = (unsigned char) ((numLostSinceLastReset << 8) / numExpectedSinceLastReset);// 这里用了一个定点数除法的技巧,拆开来看:// lossFraction = (丢包数 × 256) / 期望包数// 本质上是:// lossFraction = 丢包数 / 期望包数 × 256 = 丢包比例 × 256// 为什么乘以256(即左移8位)?// 因为 lossFraction 是 unsigned char,只能存整数,直接做除法小数部分会丢失。// RFC 3550 规定丢包率字段是一个 8 bit 定点小数,0xFF 代表 100% 丢包,0x00 代表 0% 丢包。

highestExtSeqNumReceived – 收到的最高序列号

含义

接收到的最高序列号(经过扩展以处理 16 位序列号的回绕)。

结构

  • 低 16 位:接收到的 RTP 数据包中最大的序列号。
  • 高 16 位:序列号发生"翻转/回绕"(从 65535 回到 0)的累计次数。

计算方法

接收端维护一个计数器,每当检测到序列号发生大幅度下降并重新开始时,将回绕次数加 1。

代码分析

// MultiFramedRTPSource::networkReadHandler1() 中有如下代码, 每收到一个RTP包都会对// highestExtSeqNumReceived进行更新。...receptionStatsDB().noteIncomingPacket(rtpSSRC,rtpSeqNo,rtpTimestamp,timestampFrequency(),usableInJitterCalculation,presentationTime,hasBeenSyncedUsingRTCP,bPacket->dataSize());...

Jitter – 抖动

含义

抖动(Jitter)是衡量网络质量的关键指标之一。它反映了数据包到达时间的不稳定性。

计算方法

在 RTP/RTCP 协议中(遵循 RFC 3550 标准),抖动的计算分为两步:首先计算单对包的相对偏差,然后进行平滑滤波。

第一步:计算相对偏差 D

假设有两个先后发送的包 i 和 j:

  • S_i 和 S_j 是包在发送端发送时的RTP 时间戳
  • R_i 和 R_j 是包到达接收端时的接收时刻本地时间(需转换为与 RTP 时间戳相同的单位)。

它们之间的相对偏差 D 定义为:

D(i, j) = (R_j - R_i) - (S_j - S_i) = (R_j - S_j) - (R_i - S_i)

理解:D 其实就是"包 j 的网络传输延迟"减去"包 i 的网络传输延迟"。如果 D=0,说明两个包的延迟一模一样。

第二步:计算平均抖动 J

为了防止抖动值因单个包的突发情况而剧烈波动,协议采用了一种一阶增益滤波器进行平滑处理。

每收到一个新包 j 时,当前的抖动估计值 J(i) 会更新为:

J = J + (|D(i, j)| - J)/16

这个公式等价于:

J_new = (15/16) * J_old + (1/16) * |D(i, j)|

为什么要除以 16?这是一个经验值。它使得抖动值能够平滑地跟踪网络变化,同时对偶尔的随机波动(噪声)不那么敏感。

RTCP RR 包中的抖动值

在 RTCP 的 Receiver Report (RR) 包中,Interarrival Jitter字段填入的就是上面算出来的J 的整数部分

  • 单位:它的单位和 RTP 时间戳一致。例如,如果是 8kHz 采样的音频,单位就是 1/8000秒;如果是 90kHz 采样的视频,单位就是 1/90000秒。
  • 作用:发送端看到这个值如果持续上升,就知道网络可能开始拥塞了,可以考虑降低码率。

形象化的例子

假设发送端每 10ms 发一个包,时间戳分别为 10, 20, 30。

  1. 包 1:10ms 发出,50ms 收到。
  2. 包 2:20ms 发出,65ms 收到。(延迟增加了 5ms)
    • D(1, 2) = (65 - 50) - (20 - 10) = 15 - 10 = 5
  3. 包 3:30ms 发出,70ms 收到。(延迟减小了 5ms)
    • D(2, 3) = (70 - 65) - (30 - 20) = 5 - 10 = -5

计算时会取绝对值 |D|,然后带入平滑公式。即使延迟时大时小,只要 |D| 大,最终的抖动值 J 就会显著增加。

理论参考值

抖动值 (ms)网络状态体验描述
< 20 ms极佳 (Excellent)几乎感觉不到波动,音频流畅,视频无卡顿。
20 - 50 ms良好 (Good)接收端的抖动缓冲区可以轻松处理,用户基本无感。
50 - 100 ms一般 (Fair)属于临界点。可能会感觉到轻微的语音断续,Jitter Buffer 开始动态增大。
100 - 200 ms较差 (Poor)明显的音频机械感、视频花屏或掉帧。延迟感显著增加。
> 200 ms极差 (Critical)通话基本无法进行,可能会出现长时间静音或画面冻结,连接容易断开。

代码分析

if(useForJitterCalculation&&rtpTimestamp!=fPreviousPacketRTPTimestamp){// 把系统时间转换成 RTP 时间单位unsignedarrival=(timestampFrequency*timeNow.tv_sec);// 将秒部分转换为RTP单位arrival+=(unsigned)((2.0*timestampFrequency*timeNow.tv_usec+1000000.0)/2000000);// 将微秒部分转换为RTP单位,并加上秒部分inttransit=arrival-rtpTimestamp;// 传输延迟if(fLastTransit==(~0))fLastTransit=transit;// hack for first timeintd=transit-fLastTransit;// 计算延迟差fLastTransit=transit;if(d<0)d=-d;// 取绝对值fJitter+=(1.0/16.0)*((double)d-fJitter);}

LSR – 上一次收到SR的时间戳

含义

最近收到的 Sender Report (SR) 包中的 NTP 时间戳的中间 32 位。

提取方法

发送端发送的 SR 包包含一个 64 位的 NTP 时间戳(前 32 位是秒,后 32 位是分数)。

接收端收到 SR 后,取其第 16 到 47 位(即秒的低 16 位和分数的高 16 位)存入 LSR。

如果还没收到过 SR 包,该位填 0。

代码分析

unsignedNTPmsw=stats->lastReceivedSR_NTPmsw();unsignedNTPlsw=stats->lastReceivedSR_NTPlsw();unsignedLSR=((NTPmsw&0xFFFF)<<16)|(NTPlsw>>16);// middle 32 bitsfOutBuf->enqueueWord(LSR);

DLSR – 从收到上一个 SR 到发送此 RR 之间的时间间隔

含义

从收到上一个 SR 包,到发送当前这个 RR 包之间经过了多长时间。

计算方法

  1. 记录收到上一个SR包的时刻 T_recv
  2. 确定发送当前RR包的时刻 T_sent
  3. DLSR = T_sent - T_recv

单位:1/65536秒

计算公式:DLSR = (延迟秒数) * 65536

代码分析

structtimevalconst&LSRtime=stats->lastReceivedSR_time();// "last SR"structtimevaltimeNow,timeSinceLSR;gettimeofday(&timeNow,NULL);if(timeNow.tv_usec<LSRtime.tv_usec){timeNow.tv_usec+=1000000;timeNow.tv_sec-=1;}timeSinceLSR.tv_sec=timeNow.tv_sec-LSRtime.tv_sec;timeSinceLSR.tv_usec=timeNow.tv_usec-LSRtime.tv_usec;// The enqueued time is in units of 1/65536 seconds.// (Note that 65536/1000000 == 1024/15625)unsignedDLSR;if(LSR==0){DLSR=0;}else{DLSR=(timeSinceLSR.tv_sec<<16)|((((timeSinceLSR.tv_usec<<11)+15625)/31250)&0xFFFF);}fOutBuf->enqueueWord(DLSR);

RTT的计算

RTT的计算是发生在发送端,需要用到RR中的数据,所以在这里说明一下。

发送端收到RR包后,可以通过以下公式算出往返时延(RTT):

RTT = A - LSR - DLSR

其中:

  • A:发送端收到RR的本地时间(同样取NTP中间32位单位)
  • LSR:RR包中的LSR字段
  • DLSR:RR包中的DLSR字段

原理:A - LSR算出了 "SR发出到收到RR"的总耗时,再减去 DLSR(对方处理这个包耗费的时间),剩下的就是纯粹的网络往返时间。


总结

本文详细分析了live555客户端RTCP处理机制:

  1. 核心类关系:RTPSource负责收数据,RTCPInstance负责汇报接收质量,两者通过RTPReceptionStatsDB共享统计数据。

  2. RR报告构造:包括SSRC、总丢包数、瞬时丢包率、最高序列号、抖动、LSR、DLSR等关键字段的计算方法。

  3. 关键技术点:

    • 扩展序列号解决16位序列号回绕问题
    • 抖动计算采用RFC 3550标准的一阶增益滤波器
    • LSR/DLSR配合实现RTT测量

这些统计信息对于网络质量监控、拥塞控制、码率自适应等应用场景至关重要。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询