3. RTP协议概述

聊到RTP,有个东西你绕不开——Payload Type(PT)。说白了,它就是告诉接收方:“嘿,我这包里装的是啥编码格式的数据”。没有它,接收方根本不知道该怎么解码。我刚开始接触RTP时,觉得PT就是个数字编号,后来踩过坑才明白,这里面的门道可不少。

3.1 PT字段:RTP头里的“身份证”

RTP固定头里有个7比特的PT字段,取值范围0-127。它标识了RTP数据包中负载的编码格式和采样率。接收端拿到包后,先看PT值,再决定用哪个解码器来处理。

核心要点:PT值决定了RTP流的“语义”。同一个PT值,在不同profile下可能代表不同编码。所以,SDP协商时PT值的映射关系必须一致,否则就是鸡同鸭讲。

我见过不少新手,直接把PT值硬编码成96,以为随便用就行。结果呢?两端PT映射对不上,解码全乱套。嗯,这里要注意:PT值本身只是一个索引,真正的编码参数(如采样率、声道数)是通过SDP或带外信令协商的。

3.2 静态PT vs 动态PT

PT分配分两种:静态和动态。我习惯把它们比作“官方指定”和“自由发挥”。

类型 范围 特点 典型编码
静态PT 0-95 IANA预定义,编码格式固定,无需额外协商 PCMU (0), PCMA (8), G.722 (9), L16 (10)
动态PT 96-127 通过SDP协商,编码格式由应用自定义 H.264 (96-106), Opus (111), AAC (97-99)

静态PT:IANA(互联网号码分配局)把0-95范围内的PT值固定分配给了某些经典编码。比如PT=0就是PCMU(G.711 μ-law),PT=8就是PCMA(G.711 A-law)。这些编码的采样率、声道数、帧长都是固定的,所以接收端看到PT=0,就知道该用8kHz、1声道、μ-law解码。好处是零协商,即插即用。但缺点也很明显——太死板,只支持那几种老编码。

动态PT:96-127这32个值,留给现代编码自由发挥。H.264、AAC、Opus这些“新贵”都用动态PT。为什么叫“动态”?因为同一个PT值,在不同会话里可能代表不同编码。比如PT=96,在你的系统里是H.264,在我的系统里可能是VP8。所以,必须通过SDP把PT值和编码格式绑定起来。

我的经验:做SDP协商时,动态PT的分配范围尽量靠后(比如从106开始),给中间件或转码服务留点余量。我曾经遇到过一个网关,硬编码了PT=96-100给H.264,结果和客户端的AAC冲突,排查了半天才发现是PT值撞车了。

3.3 常见编码格式与RTP封装

每种编码都有自己的“脾气”,RTP封装方式也各不相同。我挑几个最常用的,说说它们的封装要点和坑。

3.3.1 H.264:NALU单元打包

H.264在RTP里是按NALU(Network Abstraction Layer Unit)打包的。每个NALU就是一个RTP负载。H.264的RTP封装有三种模式:单NALU模式、聚合模式(STAP-A)和分片模式(FU-A)。

  • 单NALU模式:一个RTP包只装一个NALU。简单直接,适合小尺寸NALU。
  • 聚合模式(STAP-A):把多个小NALU(如SPS、PPS、SEI)塞进一个RTP包。减少包数量,提高带宽利用率。
  • 分片模式(FU-A):当一个NALU太大(超过MTU),就切成多个RTP包发送。接收端根据FU头里的序号重组。

避坑指南:我曾经在弱网环境下,发现H.264视频花屏严重。排查后发现是FU-A分片包乱序到达,接收端重组时把不完整的NALU丢给了解码器。解决方案:在RTP接收端实现NALU重组缓冲,等待所有分片到齐后再解码。另外,SDP里要明确声明packetization-mode(0=单NALU,1=非交错模式,2=交错模式),否则两端封装方式不一致,解码必崩。

3.3.2 AAC:ADTS头与Access Unit

AAC音频在RTP里封装,核心是Access Unit(AU)。每个AU就是一帧AAC数据。RTP负载里可以包含一个或多个AU。AAC的RTP封装有两种主流方式:

  • 低开销模式(Low Overhead):每个AU前加一个AU Header(2字节),描述AU长度。适合实时流。
  • 通用模式(Generic):使用AU Header Table,把多个AU的头部信息集中放在负载开头。适合文件流或存储。

AAC的采样率、声道数、帧长这些参数,通常通过SDP中的fmtp属性传递。比如:a=fmtp:97 streamtype=5; profile-level-id=1; mode=AAC-hbr; config=1190。这里的config是AudioSpecificConfig,解码器需要它才能正确初始化。

我的习惯:做AAC RTP推流时,我会在SDP里显式声明sizeLength和indexLength参数,避免接收端猜AU边界。否则,接收端只能靠ADTS头里的syncword来同步,一旦丢包,同步就丢了,音频会一直沙沙响。

3.3.3 Opus:自带FEC的“卷王”

Opus是后起之秀,RTP封装相对简单。每个RTP包可以包含多个Opus帧(最多120ms的音频)。Opus的RTP负载格式定义在RFC 6716里。它的亮点是自带前向纠错(FEC)和包丢失隐藏(PLC)。

Opus的RTP封装要点:

  • 帧打包:每个RTP包可以包含1-48个Opus帧。帧与帧之间通过自描述的长度字段分隔。
  • FEC:Opus支持带内FEC,发送端可以在当前包中携带前一帧的冗余信息。接收端丢包时,用FEC数据恢复。
  • DTX:不讲话时自动降低码率,节省带宽。

实战建议:Opus的FEC不是万能的。我在VoIP项目里测试过,丢包率超过20%时,FEC反而会加重带宽负担,导致更多丢包。所以,我一般建议:丢包率<10%时开启FEC,超过10%时改用PLC或降低码率。

3.3.4 G.711:最“古老”的PCM

G.711就是PCM编码,分μ-law(美国/日本)和A-law(欧洲/中国)。RTP封装最简单:每个RTP包直接装PCM采样点。采样率固定8kHz,每个采样8比特,所以每20ms的音频就是160字节。

G.711的RTP封装几乎没有歧义。但有个细节:静音检测(VAD)。很多老系统在静音时不发RTP包,接收端靠CNG(舒适噪声)填充。如果两端VAD策略不一致,就会出现“断断续续”的感觉。

我曾经踩过的坑:有一次对接一个老式PBX,对方用G.711 A-law,但静音时发的是全0的RTP包(不是CNG)。我这边解码后,静音段全是“噗噗”的噪声。后来我在接收端加了静音检测逻辑:连续收到全0包超过3个,就自动插入舒适噪声。问题解决。

3.4 核心逻辑:PT与编码的映射关系

为了让你更直观地理解PT、编码、封装之间的关系,我画了一张图。这张图展示了从编码器到RTP包的完整流程,以及PT值在其中扮演的角色。

RTP负载类型与编码映射关系图 RTP负载类型与编码映射关系 编码器 H.264 / AAC / Opus 编码数据 NALU / AU / 帧 RTP封装 单包/聚合/分片 RTP包 PT + 负载 PT分配:静态(0-95) / 动态(96-127) 通过SDP协商绑定编码格式 解码器:根据PT值选择对应解码器 H.264解码器 / AAC解码器 / Opus解码器 💡 关键点: PT值只是一个索引,真正的编码参数(采样率、声道数、帧长)通过SDP的fmtp属性传递。 接收端必须根据PT值找到正确的解码器,并读取SDP中的编码参数,才能正确解码。

这张图把整个流程串起来了:编码器产生原始数据 → 封装成RTP负载 → 分配PT值 → 打包成RTP包发送 → 接收端根据PT值选择解码器。PT值就是中间的“翻译官”,没有它,两端就是鸡同鸭讲。

3.5 实战:SDP中的PT协商示例

说了这么多理论,来点实际的。下面是一个典型的SDP片段,展示了如何协商动态PT:

m=video 5004 RTP/AVP 96 97
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=42e01f; sprop-parameter-sets=Z0IACpY1QKAB5oQ==,aM4G4g==
a=rtpmap:97 MP4V-ES/90000
a=fmtp:97 profile-level-id=3; config=000001B00300000001000000012000C88880F0A0

m=audio 5006 RTP/AVP 98 99
a=rtpmap:98 opus/48000/2
a=fmtp:98 minptime=10; useinbandfec=1
a=rtpmap:99 MPEG4-GENERIC/48000/2
a=fmtp:99 streamtype=5; profile-level-id=1; mode=AAC-hbr; config=1190; sizeLength=13; indexLength=3; indexDeltaLength=3

你看,视频流用了PT=96(H.264)和PT=97(MPEG-4 Visual),音频流用了PT=98(Opus)和PT=99(AAC)。每个PT后面都跟着rtpmap和fmtp,把编码参数交代得清清楚楚。

我的建议:写SDP时,fmtp参数别偷懒。尤其是H.264的sprop-parameter-sets(包含SPS和PPS的Base64编码),少了它,接收端连SPS/PPS都拿不到,解码直接失败。我见过有人图省事,把SPS/PPS放在RTP带内发送,结果丢包后新加入的接收端永远解不出第一帧。

3.6 常见问题与避坑

  • PT值冲突:不同媒体流用了相同的动态PT值,但映射了不同编码。解决方案:每个媒体流独立分配PT范围,避免跨流复用。
  • 静态PT误用:把动态编码(如Opus)分配给了静态PT范围(如PT=10)。接收端会按L16解码,声音完全失真。解决方案:严格遵循IANA分配,动态编码只用96-127。
  • 封装模式不匹配:发送端用FU-A分片,接收端却期望单NALU模式。解决方案:SDP中明确声明packetization-mode,并在会话建立前确认双方支持。
  • FEC与带宽的平衡:Opus的带内FEC虽然好,但会额外消耗30%左右的带宽。在带宽受限的场景下,我建议关闭FEC,改用PLC或降低码率。

最后提醒一句:PT值只是RTP协议里一个小小的字段,但它牵一发而动全身。分配错了,整个会话就废了。我的习惯是:在SDP协商完成后,打印出完整的PT映射表,人工核对一遍。虽然麻烦,但能避免90%的“低级错误”。