RTP/RTCP 协议基础:RTP 包结构、RTCP SR/RR 报告、NACK 与 PLI

聊到 WebRTC 流控,RTP 和 RTCP 是绕不开的基石。说白了,RTP 负责把音视频数据从 A 点搬到 B 点,RTCP 则负责在背后打小报告——质量怎么样、丢了多少包、要不要重传。我刚开始接触 WebRTC 时,总觉得 RTCP 是可有可无的辅助协议,直到有一次线上会议频繁卡顿,抓包一看才发现 RTCP 报告里全是异常。嗯,从那以后我再也不敢小看它了。

3.1 RTP 包结构:数据搬运工的身份证

RTP(Real-time Transport Protocol)是实时传输的载体。每个 RTP 包都像快递包裹,头部信息就是快递单。你想想看,没有快递单,接收端怎么知道这包数据是视频第几帧、音频第几毫秒?

一个标准的 RTP 头部长这样:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|            contributing source (CSRC) identifiers             |
|                             ....                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

几个关键字段,我挑重要的说:

  • V(版本号):固定为 2,目前主流版本。
  • P(填充位):如果置 1,包尾有填充字节。我见过一些加密实现会用这个字段对齐块大小。
  • X(扩展位):置 1 表示头部后面跟着扩展头。WebRTC 的 Transport-cc 扩展就在这里。
  • CC(CSRC 计数):表示后面有多少个 CSRC 标识符。混音场景下会用,WebRTC 里很少见。
  • M(标记位):视频里通常标记帧边界,音频里标记会话开始。
  • PT(负载类型):告诉接收端这包数据是 H264、VP8、Opus 还是别的。WebRTC 动态协商 PT 值,范围 96-127。
  • sequence number(序列号):16 位,每发一个包加 1。接收端靠它检测丢包、排序。我调试时最常看的就是这个字段——序列号跳变基本等于丢包。
  • timestamp(时间戳):32 位,音频和视频的时钟频率不同。音频通常 48kHz,视频 90kHz。这个字段决定了播放时机。
  • SSRC(同步源标识符):32 位随机数,唯一标识一个 RTP 流。同一个会话里,每个音视频流都有自己的 SSRC。
我的习惯:调试丢包时,我会先看序列号是否连续。如果连续但画面卡顿,问题可能在时间戳或 jitter buffer。如果序列号跳了,那就是网络丢包,得看 RTCP 报告确认丢包率。

3.2 RTCP SR/RR 报告:收发双方的质量体检单

RTCP(RTP Control Protocol)周期性发送控制包。它不像 RTP 那样每帧都发,而是按带宽比例分配——我建议你记住这个比例:RTCP 流量通常控制在会话总带宽的 5% 以内。

RTCP 有好几种类型,流控里最核心的是 SR(Sender Report) 和 RR(Receiver Report)。

3.2.1 SR 包:发送端自报家门

SR 由数据发送方发出。它包含两部分:

  • 发送端信息:NTP 时间戳(绝对时间)、RTP 时间戳(相对时间)、发送包计数、发送字节计数。
  • 报告块:针对每个接收端的质量统计,包括丢包率、累计丢包数、最高序列号、抖动、延迟等。

SR 包结构大致如下:

+-------------------------------+
| 头部 (V=2, P, RC, PT=200)    |
+-------------------------------+
| SSRC of sender                |
+-------------------------------+
| NTP timestamp (most significant)|
+-------------------------------+
| NTP timestamp (least significant)|
+-------------------------------+
| RTP timestamp                 |
+-------------------------------+
| sender's packet count         |
+-------------------------------+
| sender's octet count          |
+-------------------------------+
| 报告块 1 (针对接收端 A)       |
| 报告块 2 (针对接收端 B)       |
| ...                           |
+-------------------------------+

关键指标解读:

  • 丢包率(fraction lost):8 位,表示上一个报告周期内的丢包比例。值 0-255,对应 0%-100%。我一般看到超过 10%(约 25)就认为网络质量较差。
  • 累计丢包数(cumulative lost):24 位,从会话开始到现在的总丢包数。这个值只增不减,适合看长期趋势。
  • 抖动(interarrival jitter):32 位,表示包到达时间的统计方差。抖动大说明网络不稳定,jitter buffer 需要调整。
  • 延迟(LSR/DLSR):LSR 是上次收到 SR 的 NTP 时间,DLSR 是从收到 SR 到发出 RR 的延迟。两者结合可以算出 RTT。

3.2.2 RR 包:接收端反馈实况

RR 包由数据接收方发出。它和 SR 的报告块格式完全一样,只是没有发送端信息部分。RR 包告诉发送方:我这边收到了多少、丢了多少、抖动多大、延迟多长。

我举个例子。有一次线上会议,用户反馈声音断断续续。我抓包发现 RR 报告里的丢包率从 2% 突然跳到 15%,同时抖动值飙升。这说明网络瞬间恶化,jitter buffer 来不及适应。后来我们加了 NACK 重传,情况才好转。

字段 长度 说明
fraction lost 8 bit 上一个周期丢包比例,0-255
cumulative lost 24 bit 累计丢包数
highest seq no. 32 bit 收到的最高序列号
interarrival jitter 32 bit 到达时间抖动
LSR 32 bit 上次收到 SR 的 NTP 时间
DLSR 32 bit 从收到 SR 到发出 RR 的延迟
我曾经踩过的坑:RR 报告里的丢包率是周期性的,不是瞬时值。如果你只看一次报告就做流控决策,很容易误判。我建议至少看 3-5 个连续报告周期,确认趋势后再调整码率。

3.3 NACK 与 PLI:丢包后的急救措施

RTP 是 UDP 传输,天生不可靠。丢包了怎么办?WebRTC 提供了两种反馈机制:NACK 和 PLI。

3.3.1 NACK:精准补包

NACK(Negative ACKnowledgment)是 RTCP 的一种反馈包(PT=205),FMT=1。接收端发现某个序列号丢了,就发 NACK 告诉发送端:把第 X 号包重传给我。

NACK 包结构:

+-------------------------------+
| 头部 (V=2, P, FMT=1, PT=205) |
+-------------------------------+
| SSRC of sender                |
+-------------------------------+
| SSRC of media source          |
+-------------------------------+
| 丢失的序列号 (PID)            |
| 位掩码 (BLP)                  |
+-------------------------------+
  • PID(Packet ID):丢失的起始序列号。
  • BLP(Bitmask of Lost Packets):16 位掩码,表示 PID 之后哪些包也丢了。位 0 表示 PID+1,位 1 表示 PID+2,以此类推。

一个 NACK 包最多可以报告 17 个丢包(1 个 PID + 16 个掩码位)。我调试时经常用 Wireshark 过滤 rtcp.nack,看丢包模式是零星丢包还是连续丢包。

我的建议:NACK 不是万能的。如果丢包率超过 20%,NACK 重传反而会加剧网络拥塞。这时候应该降码率,而不是拼命重传。WebRTC 的 Pacer 模块会控制重传节奏,但底层逻辑还是靠 NACK 触发。

3.3.2 PLI:关键帧请求

PLI(Picture Loss Indication)是另一种 RTCP 反馈(PT=206,FMT=1)。它不要求重传某个包,而是要求发送端立即生成一个关键帧(I 帧)。

什么时候用 PLI?

  • 接收端刚加入会话,没有参考帧。
  • 连续丢包导致参考帧损坏,解码器无法恢复。
  • 视频画面突然花屏,解码器请求重置。

PLI 包结构很简单:

+-------------------------------+
| 头部 (V=2, P, FMT=1, PT=206) |
+-------------------------------+
| SSRC of sender                |
+-------------------------------+
| SSRC of media source          |
+-------------------------------+

没有额外字段,就是一个信号:给我一个关键帧。

注意:PLI 不能滥用。关键帧通常比普通帧大几倍甚至十几倍,频繁请求关键帧会导致带宽暴涨。我见过一个极端案例:接收端每 100ms 发一次 PLI,发送端被迫每秒生成 10 个关键帧,码率直接飙到 20Mbps。后来我们在代码里加了 PLI 频率限制——同一 SSRC 每秒最多 1 次 PLI。

3.4 知识体系总览

下面这张图把 RTP/RTCP 的核心关系串起来了。我建议你多看几遍,理解数据流和反馈流是怎么配合的。

RTP/RTCP 核心交互流程 发送端 接收端 RTP 数据流 序列号、时间戳、SSRC RTCP SR(发送报告) RTCP RR(接收报告) NACK(丢包重传请求) PLI(关键帧请求) 说明: RTP 承载音视频数据,RTCP 承载控制信息。SR/RR 提供质量统计,NACK 精准补包,PLI 请求关键帧。 流控算法依赖这些反馈来调整发送码率和重传策略。

这张图里,RTP 是单向数据流,RTCP 是双向控制流。SR 从发送端到接收端,RR、NACK、PLI 从接收端到发送端。流控算法就是根据 RR 里的丢包率、抖动、RTT,以及 NACK 的频率,动态调整发送码率。

我个人习惯在调试时先确认 RTCP 报告是否正常到达。如果接收端不发 RR,发送端就不知道网络状况,只能盲目发送。这种情况我遇到过好几次——最后发现是防火墙阻断了 RTCP 端口。