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这个“外援”。流程大致分三步:

  1. Offer端发起:发送方列出支持的编码列表,每个编码绑定一个动态PT值。
  2. Answer端回应:接收方从列表中选择一个或多个它支持的编码,确认PT映射。
  3. 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值的分类、范围、协商方式、典型编码串在了一起。你可以把它当作一个快速参考。

RTP负载类型(PT)映射体系 PT值范围:0–127 静态PT:0–95(RFC 3551) 动态PT:96–127(SDP协商) 音频编码 视频编码 其他/保留 PT=0 PCMU PT=8 PCMA PT=3 GSM PT=14 MPA PT=26 JPEG PT=32 MPV PT=34 H263 音频编码 视频编码 文本/应用 96=Opus 97=AAC 96=H.264 98=VP8 99=H.265 注意:动态PT的编号不固定,同一个编码在不同会话中可能绑定不同的PT值,完全由SDP协商决定

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反馈时,你会觉得顺畅很多。