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的核心要素分成了三块:头部字段、可选字段和功能说明。你可以看到,SSRC和CSRC属于头部字段,而Sequence Number和Timestamp则直接决定了接收端能否正确还原数据流。
调试建议:在GStreamer中,你可以用GST_DEBUG=rtp*:5来打印RTP包的详细信息。我曾经用这个命令抓到了一个timestamp溢出的bug——某个摄像头在运行48小时后,timestamp回绕导致画面卡死。加上rtpjitterbuffer的rtp-timestamp-offset属性后,问题解决。