4、RTP与RTCP协议详解:RTP头部结构、RTCP SR/RR报告、NACK与PLI/FIR消息、扩展头部与元数据

聊到WebRTC的丢包恢复,绕不开两个最基础的协议——RTP和RTCP。很多刚入行的朋友觉得这就是个“包头+负载”的事,其实不然。我在早期做P2P视频通话时,就因为对RTP头部的理解不够深,导致一个兼容性问题排查了整整两天。嗯,今天咱们就把这块彻底掰开揉碎。

核心认知:RTP负责“运数据”,RTCP负责“反馈路况”。丢包恢复的所有策略,都建立在这两个协议的交互之上。

4.1 RTP头部结构——数据包的“身份证”

RTP头部虽然只有12字节固定部分,但每个字段都有讲究。你想想看,一个视频帧可能被拆成几十个RTP包,接收端怎么把它们拼回去?靠的就是头部里的序列号和时间戳。

字段 长度(bit) 作用
V (版本) 2 当前固定为2
P (填充) 1 为1时尾部有填充字节
X (扩展) 1 为1时头部后有扩展头
CC (CSRC计数) 4 表示CSRC标识符数量
M (标记) 1 视频关键帧或音频帧边界
PT (负载类型) 7 标识编码格式,如H264=96
序列号 16 递增,检测丢包的核心依据
时间戳 32 采样时刻,用于播放同步
SSRC 32 同步源标识,唯一标识发送端
CSRC列表 0~96 混音场景下的贡献源列表

我个人习惯把序列号比作“快递单号”,时间戳则是“生产日期”。接收端通过序列号连续性判断是否丢包,通过时间戳决定何时播放。这里有个坑——时间戳的单位不是毫秒,而是采样率相关的步进。比如音频8kHz采样,每帧20ms,时间戳增量就是160。我曾经见过有人直接用毫秒当时间戳,结果音视频同步全乱套。

实战技巧:在调试时,我经常用Wireshark过滤 rtp,然后观察序列号跳变。如果看到连续跳了5个以上,基本可以断定网络有丢包。这时候就该RTCP上场了。

4.2 RTCP SR/RR报告——发送端与接收端的“对账本”

RTCP包有很多类型,但日常打交道最多的就是SR(发送端报告)和RR(接收端报告)。SR由发送端发出,RR由接收端发出。说白了,这就是双方在定期对账。

SR里包含什么?最重要的就是NTP时间戳和RTP时间戳的对应关系。接收端拿到这个,才能把RTP时间戳映射到绝对时间,实现音视频唇同步。我做过一个测试,如果SR间隔超过5秒,音视频同步误差就会明显到让人不适。

RR里则藏着丢包恢复的关键信息——丢包率、累计丢包数、抖动、最后收到的序列号和时间戳。发送端看到丢包率超过阈值,就该调整码率或触发重传了。

注意:RTCP的发送间隔有严格限制。RFC 3550规定,RTCP带宽不能超过会话总带宽的5%。如果RTCP发得太频繁,反而会加剧网络拥塞。我在项目中就踩过这个坑——为了快速反馈丢包,把RTCP间隔设到100ms,结果带宽被控制报文吃掉了15%,视频质量反而下降。

4.3 NACK与PLI/FIR消息——丢包恢复的“三板斧”

这三类消息是WebRTC丢包恢复的核心武器。咱们一个一个说。

4.3.1 NACK(否定确认)

NACK是RTCP的一种扩展类型(PT=205)。接收端发现序列号不连续时,就发NACK告诉发送端:“我丢了包序号X,快重传!”

NACK包的结构很简单:

// NACK包格式(简化)
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|   FMT=1  |   PT=205     |          长度                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                SSRC of packet sender                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                SSRC of media source                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           丢失包序列号         |  丢失位掩码(16bit)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

注意那个“丢失位掩码”——它用16位表示当前丢失包之后16个包的丢失情况。这样一条NACK可以报告最多17个丢包,效率很高。我建议你在实现时,把NACK的触发阈值设为连续丢2个包以上,单个丢包可能是乱序,直接重传反而浪费带宽。

4.3.2 PLI(图片丢失指示)

PLI是RTCP的PSFB类型(PT=206,FMT=1)。当接收端发现视频帧损坏严重(比如关键帧丢了),解码器无法正常工作,就会发PLI请求一个完整的关键帧。

PLI不指定具体丢哪个包,它只说“我解码器崩了,给我来个关键帧重置一下”。发送端收到后,会强制编码一个关键帧发出。代价是码率会突然飙升,但总比一直花屏强。

4.3.3 FIR(全帧内请求)

FIR和PLI很像,但更“强硬”。FIR是RFC 5104定义的,它要求发送端必须立即生成一个关键帧。PLI只是“建议”,发送端可以忽略;但FIR带有优先级和序列号,发送端必须响应。

在实际项目中,我通常这样用:

  • 普通丢包 → NACK重传
  • 关键帧丢失或解码器错误 → PLI
  • 新加入用户或切换流 → FIR

经验之谈:不要滥用PLI和FIR。每次请求关键帧都会导致码率尖峰,如果多人同时请求,发送端可能瞬间过载。我曾经在一个50人会议中,因为网络抖动导致所有人同时发PLI,发送端CPU直接飙到100%。后来加了去重和限速逻辑才解决。

4.4 扩展头部与元数据——RTP的“附加信息区”

RTP头部有个X位,为1时就表示后面跟着扩展头。WebRTC大量使用这个机制来携带元数据。最常见的是一对一映射扩展(one-byte header extension),格式如下:

// 一对一扩展头部格式
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      0xBEDE   |          长度(以4字节为单位)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ID=1 | 长度=0|  ID=2 | 长度=1|   数据(2字节)                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

WebRTC里常用的扩展ID包括:

  • ID=1:音频电平(RFC 6464)
  • ID=3:时间偏移(用于绝对发送时间)
  • ID=5:视频旋转角度
  • ID=10:Transport-CC序列号(用于带宽估计)

我个人觉得,Transport-CC扩展是WebRTC拥塞控制的关键。它给每个RTP包打上一个传输级序列号,接收端通过这个序列号计算延迟和丢包,反馈给发送端做码率调整。没有这个扩展,WebRTC的GCC算法就玩不转。

调试建议:如果你在Chrome的chrome://webrtc-internals里看到googRtt和googPacketsLost,这些数据就来自RTCP RR和Transport-CC反馈。我每次排查网络问题时,第一件事就是看这两个指标。

4.5 本章知识体系总览

下面这张图把RTP/RTCP的核心关系梳理清楚了。你可以把它当作本章的“地图”,随时回来对照。

RTP/RTCP 协议体系与丢包恢复关系图 RTP 数据流 RTCP 控制流 固定头部(12B) 扩展头部 序列号+时间戳 SR / RR 报告 NACK / PLI / FIR 丢包率/抖动/延迟 RTP 携带数据 RTCP 反馈丢包/请求重传 丢包恢复流程 1. 接收端检测序列号不连续 → 发送 NACK 2. 发送端收到 NACK → 重传对应 RTP 包 3. 关键帧丢失 → 发送 PLI / FIR 4. 发送端收到 PLI/FIR → 强制生成关键帧

从这张图可以看得很清楚:RTP负责把音视频数据从A运到B,RTCP则负责把网络状况和丢包信息反馈回来。两者配合,才能实现动态的丢包恢复策略。没有RTCP的反馈,发送端就是个“瞎子”,只能盲目发包;没有RTP的序列号和时间戳,接收端连丢没丢包都不知道。