RTP协议核心:RTP Header结构、Payload Type、SSRC与CSRC、Sequence Number与Timestamp

聊到流媒体传输,RTP(实时传输协议)是绕不开的核心。说白了,它就是音视频数据在网络上跑的「集装箱」。我刚开始接触RTP时,觉得不就是个包头嘛,后来在调试一个视频卡顿问题时,才发现每一个字段都可能成为性能瓶颈。这一章,咱们就把RTP的底裤扒干净。

RTP Header结构:12字节的硬核基础

RTP头部固定12字节,不多不少。为什么是12?因为设计者希望它在带宽和功能之间取得平衡。你想想看,每发送一个包都要带上头部,如果头部太大,有效载荷就少了;如果太小,又装不下必要信息。

来看一下标准结构:

 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             |
|                             ....                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

每个字段都有它的使命。我习惯把前12字节称为「铁三角」——版本、类型、序列号和时间戳构成了RTP的骨架。

字段 位数 作用
V(版本) 2 当前版本为2,几乎不会变
P(填充) 1 表示尾部有填充字节,用于对齐
X(扩展) 1 头部是否有扩展
CC(CSRC计数) 4 后面跟着几个CSRC标识符
M(标记) 1 用于标记帧边界,比如视频关键帧
PT(载荷类型) 7 标识音视频编码格式
Sequence Number 16 包序号,用于检测丢包和排序
Timestamp 32 时间戳,用于播放同步
SSRC 32 同步源标识符,唯一标识一个流
CSRC 32×N 贡献源列表,用于混音场景

核心要点:RTP头部虽然只有12字节,但每个bit都有讲究。我在做低延迟直播时,曾经为了省带宽,尝试压缩头部,结果发现得不偿失——接收端无法正确重组数据流。所以,别打这12字节的主意。

Payload Type:编码格式的身份证

PT字段只有7位,却能标识128种不同的载荷类型。标准中定义了一些固定的映射关系,比如PT=0表示PCMU音频,PT=96-127留给动态分配。

我遇到过一个问题:两个设备协商好了用H.264,但发送端填的PT是96,接收端却期待98。结果画面一片花屏。后来我养成了习惯——在SDP协商阶段就把PT映射关系确认清楚。

常见的静态PT值:

  • 0:PCMU(G.711 μ-law)
  • 3:GSM
  • 8:PCMA(G.711 A-law)
  • 10:L16(线性PCM)
  • 26:JPEG(MJPEG)
  • 34:H.263
  • 96-127:动态分配(H.264、AAC、Opus等)

实战技巧:在GStreamer中,你可以用capsfilter强制指定PT值。比如video/x-h264, payload=96。我曾经在对接第三方SDK时,对方只认PT=98,我就在pipeline里加了个capsfilter搞定。

SSRC与CSRC:谁在说话?

SSRC(同步源)是一个32位的随机数,用来唯一标识一个RTP流。同一个会话中,每个流的SSRC必须唯一。如果两个流撞了SSRC,接收端就会混乱——它分不清哪个包属于哪个流。

CSRC(贡献源)则用于混音场景。比如一个视频会议中,有3个人同时说话,混音器会把他们的音频混合成一个流,然后在CSRC列表中列出这3个人的SSRC。接收端就知道:「哦,这个包里有3个人的声音」。

我记得有一次调试一个多方通话系统,发现某个用户的音频总是断断续续。抓包一看,CSRC列表里居然有重复的SSRC。原来是混音器在添加贡献源时没做去重。修复后,问题消失。

避坑指南:我曾经在实现自定义RTP发送器时,忘记设置SSRC,结果GStreamer的接收端直接丢弃了所有包。因为rtpjitterbuffer要求每个流必须有有效的SSRC。所以,SSRC不能为0,也不能重复。

Sequence Number与Timestamp:时间与顺序的守护者

这两个字段是RTP最核心的机制。Sequence Number用于检测丢包和恢复顺序,Timestamp用于播放同步。

Sequence Number从随机值开始,每发送一个RTP包就加1。接收端通过检查序列号的连续性来判断是否丢包。如果收到1、2、4,就知道3丢了。

Timestamp则反映采样时刻。对于音频,它通常按采样率递增;对于视频,它按帧率递增。注意:Timestamp的增量不是固定的——如果视频编码器跳帧了,Timestamp的差值就会变大。

我踩过一个坑:在GStreamer中,如果timestamp设置得不合理,rtpjitterbuffer会认为包乱序了,然后疯狂重排序,导致延迟飙升。后来我严格按照采样率计算timestamp,问题解决。

举个例子,假设音频采样率是8000Hz,每帧20ms:

第一帧:timestamp = 0
第二帧:timestamp = 160  (20ms × 8000Hz / 1000)
第三帧:timestamp = 320
...

对于视频,假设帧率是30fps,时钟频率是90000Hz:

第一帧:timestamp = 0
第二帧:timestamp = 3000  (90000 / 30)
第三帧:timestamp = 6000
...

为什么视频要用90000Hz?因为H.264等编码器支持多种帧率,90000是它们的公倍数,可以精确表示各种时间戳。音频则直接用采样率,因为采样率本身就是时间基准。

知识体系总览

下面这张图把RTP核心要素串起来了。你可以看到,从发送端到接收端,每个字段都在扮演自己的角色。

RTP核心要素关系图 RTP核心要素关系图 发送端 接收端 RTP数据包 12字节固定头部 + 载荷 头部字段 V / P / X / CC / M PT / Sequence Number Timestamp / SSRC 可选字段 CSRC列表 头部扩展 填充字节 核心功能 • 载荷类型识别 • 丢包检测与排序 • 时间同步 • 流标识与混音 常见问题 • SSRC冲突 • PT不匹配 • 时间戳跳跃 • 序列号回绕 每个字段都影响最终的播放质量

这张图把RTP的核心要素分成了三块:头部字段、可选字段和功能说明。你可以看到,SSRC和CSRC属于头部字段,而Sequence Number和Timestamp则直接决定了接收端能否正确还原数据流。

调试建议:在GStreamer中,你可以用GST_DEBUG=rtp*:5来打印RTP包的详细信息。我曾经用这个命令抓到了一个timestamp溢出的bug——某个摄像头在运行48小时后,timestamp回绕导致画面卡死。加上rtpjitterbuffer的rtp-timestamp-offset属性后,问题解决。