3. RTP负载类型与编码映射:PT值定义、静态与动态协商
这一章我们聊聊RTP里一个特别基础、但又特别容易踩坑的东西——负载类型(Payload Type,简称PT)。
说白了,PT值就是告诉接收方:“嘿,我这包数据用的是哪种编码格式”。是G.711还是H.264?是Opus还是MPEG?全凭这个7位的数字来标识。
我刚开始接触RTP时,总觉得PT就是个简单的编号。后来在项目中排查一个音视频不同步的问题,折腾了两天才发现,原来是PT值协商没对齐,接收方用A律解码器去解μ律的流……嗯,从那以后我再也不敢小看这个字段了。
3.1 PT字段在RTP头中的位置
先看一眼PT字段在RTP头部里的位置。RTP固定头一共12字节,PT占第2个字节的低7位。
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 |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
PT字段只有7位,取值范围0–127。其中0–95是静态分配,96–127留给动态协商。这个划分很关键,我后面会细说。
3.2 静态负载类型:RFC 3551的“老规矩”
静态负载类型,就是IANA和RFC 3551提前定好的编码映射。你只要看到PT=0,就知道是PCMU(G.711 μ律);看到PT=8,就是PCMA(G.711 A律)。不需要额外协商,开箱即用。
我个人习惯把常用的静态PT值记在脑子里,调试时抓个包扫一眼就能判断编码格式,不用每次都翻文档。
| PT值 | 编码名称 | 时钟频率(Hz) | 声道数 | 描述 |
|---|---|---|---|---|
| 0 | PCMU | 8000 | 1 | G.711 μ律,64 kbps |
| 3 | GSM | 8000 | 1 | GSM 6.10,13 kbps |
| 4 | G723 | 8000 | 1 | G.723.1,5.3/6.3 kbps |
| 8 | PCMA | 8000 | 1 | G.711 A律,64 kbps |
| 9 | G722 | 8000 | 1 | G.722,48/56/64 kbps |
| 10 | L16-2 | 44100 | 2 | 16位线性PCM,立体声 |
| 14 | MPA | 90000 | 1 | MPEG-1/2 Audio |
| 26 | JPEG | 90000 | - | MJPEG视频 |
| 32 | MPV | 90000 | - | MPEG-1/2 Video |
| 34 | H263 | 90000 | - | H.263视频 |
关键点:静态PT值0–95中,很多编号其实没有分配。实际常用的也就十几个。不要以为所有0–95都有定义,查IANA注册表时你会发现大量“Unassigned”。
你想想看,为什么音频的时钟频率大多是8000 Hz,而视频却是90000 Hz?
因为音频采样率本身就在8k附近,直接用就行。视频的帧率通常是25/30 fps,用90000 Hz作为时钟基准,可以精确到1/90000秒,做时间戳计算时不容易丢精度。这个设计其实挺巧妙的。
3.3 动态负载类型:96–127的“自由地带”
静态PT只覆盖了最老牌的编码格式。像H.264、Opus、VP8、VP9这些后来者,都没有静态分配。它们必须走动态协商。
动态PT的范围是96–127,一共32个编号。注意,这个范围不是“某个编码固定占某个号”,而是通过SDP(Session Description Protocol)在会话建立时临时绑定。
举个例子,一个典型的SDP片段长这样:
m=audio 5004 RTP/AVP 96 97
a=rtpmap:96 opus/48000/2
a=rtpmap:97 G722/8000/1
这里96被映射成了Opus,97被映射成了G.722。换一个会话,96可能就变成H.264了。完全由双方协商决定。
我的建议:在实现动态PT协商时,尽量把最常用的编码放在较小的动态编号上。比如Opus用96,H.264用97。虽然协议没有强制,但这样抓包时一眼就能认出来,调试效率高很多。
3.4 动态协商的完整流程
动态协商不是RTP自己完成的,它依赖SDP这个“外援”。流程大致分三步:
- Offer端发起:发送方列出支持的编码列表,每个编码绑定一个动态PT值。
- Answer端回应:接收方从列表中选择一个或多个它支持的编码,确认PT映射。
- RTP流发送:双方按协商好的PT值封装数据,接收方根据PT查表找到对应的解码器。
我曾经在一个项目中遇到过这样的坑:Offer端发了96=Opus和97=G.722,Answer端只回了96=Opus。按理说没问题,但Answer端在SDP里把96的rtpmap写成了opus/48000,漏掉了/2(双声道)。结果Opus解码器初始化时默认用了单声道,播放出来声音变调了……
避坑指南:SDP中的rtpmap行,编码参数(如声道数、时钟频率)必须与编码规范完全一致。少一个参数、多一个空格,都可能导致解码器行为异常。我曾经因为opus/48000/2写成了opus/48000,排查了整整一个下午。
3.5 PT值映射的完整知识图谱
下面这张图把PT值的分类、范围、协商方式、典型编码串在了一起。你可以把它当作一个快速参考。
3.6 实际项目中的PT处理要点
讲完了理论,我分享几个实战中容易忽略的点。
- 不要硬编码动态PT:我见过有人把96直接写成
#define PT_H264 96,结果换了个终端,对方把H.264绑到了98上,直接解码失败。动态PT必须从SDP解析结果中读取,不能写死。 - 注意PT值的重复:同一个RTP会话中,不同编码必须使用不同的PT值。如果你同时发Opus和G.722,不能都用96。
- RTCP也依赖PT:RTCP包的类型也是通过PT区分的。PT=200是SR,PT=201是RR,PT=202是SDES。不要和RTP的PT搞混了。
- Payload Type != Codec:PT只是编码的标识符,不是编码本身。同一个编码(比如H.264)可以有不同的封装参数(packetization-mode、profile-level-id),这些信息在SDP的fmtp行里,不在PT里。
调试小技巧:用Wireshark抓RTP包时,如果静态PT能正确显示编码名称,但动态PT只显示“DynamicRTP-Type-96”,说明Wireshark没有拿到SDP信息。你可以手动添加SDP描述,或者用rtp_pt过滤表达式配合解码配置。
3.7 一个简单的PT解析示例
假设你从RTP包中读到了PT字段,怎么判断它是什么编码?下面是一个C语言的示意代码,只做逻辑演示:
#include <stdint.h>
// 静态PT映射表(简化版)
typedef struct {
uint8_t pt;
const char *name;
int clock_rate;
} pt_entry;
pt_entry static_pt_table[] = {
{0, "PCMU", 8000},
{8, "PCMA", 8000},
{3, "GSM", 8000},
{14, "MPA", 90000},
{26, "JPEG", 90000},
{32, "MPV", 90000},
{34, "H263", 90000},
{0, NULL, 0} // 结束标记
};
const char* lookup_pt_name(uint8_t pt) {
// 静态范围:0-95
if (pt <= 95) {
for (int i = 0; static_pt_table[i].name != NULL; i++) {
if (static_pt_table[i].pt == pt) {
return static_pt_table[i].name;
}
}
return "Unknown-Static";
}
// 动态范围:96-127
// 实际项目中,这里应该查SDP解析出的动态映射表
return "Dynamic-Negotiated";
}
这段代码只是演示思路。真实项目中,动态PT的映射表需要在SDP解析阶段构建,用一个哈希表或者数组存起来,然后在RTP收包时查表。
好了,关于RTP负载类型与编码映射,核心内容就这些。PT值虽然只是头部里的7个比特,但它连接着编码格式、时钟频率、SDP协商、解码器选择一整条链路。把这个环节理清楚,后面讲RTP时间戳、SSRC、RTCP反馈时,你会觉得顺畅很多。