RTP协议详解:从头部字段到SIP通话与WebRTC实战
RTP 这个词,第一次接触的人多半是在两个完全不相干的场景里撞见它的。一个场景是搞音视频开发,翻 RFC 3550,被 SSRC、时间戳、序列号这些字段绕得头晕;另一个场景是打开某个老游戏,弹出一句“rpgvx rtp is not found”,一脸茫然地到处搜。这两个 RTP 其实八竿子打不着——前者是 Real-time Transport Protocol,实时传输协议;后者是 RPG Maker 系列运行库里的一个组件名。偏偏搜索引擎把它们混在一起,导致想学协议的人搜到游戏报错,想修游戏的人搜到一堆 RFC 文档。
这篇东西我打算把主线放在真正的 RTP 协议上,从它到底解决什么问题讲起,把头部字段逐个拆开,再聊它和 SIP 怎么配合、实际项目里怎么落地。顺带把那个游戏报错的坑也讲清楚,免得你搜资料的时候被带偏。如果你在做音视频通话、直播、监控流媒体,或者只是单纯想搞明白“为什么我的语音通话会卡”,这篇应该能给你一些能直接上手的东西。
1. 先搞清楚 RTP 到底在传什么
1.1 它不负责“传”,只负责“描述怎么传”
很多人对 RTP 的第一个误解,是以为它像 TCP 一样是个完整的传输方案。不是的。RTP 本身只是一个 数据格式约定 ,它规定了音视频数据包应该长什么样、每个包头上要带哪些信息。真正把包发出去的活儿,是它下面的 UDP(绝大多数情况)或者 TCP 干的。
打个比方:RTP 像是快递单的填写规范——收件人、发件人、包裹编号、寄出时间都写在哪个位置,格式统一。但真正把包裹从 A 送到 B 的是快递公司,也就是 UDP。RTP 不保证包裹一定送到,也不保证按顺序送到,它只保证“如果送到了,接收方知道这个包裹是什么、属于哪一批、该在什么时候播放”。
这个设计看起来很不靠谱,但恰恰是实时音视频需要的。想象一下你在打语音电话,网络抖动导致第 5 个包丢了。如果用 TCP,它会停下来等第 5 个包重传,后面的包全部堵住,你听到的就是越来越长的延迟,最后变成对讲机式的“你说完我再说”。而 RTP over UDP 的做法是:第 5 个包丢了就丢了,接收方根据序列号发现缺了一个,直接跳过,继续播第 6、7、8 个。丢一个 20ms 的音频包,人耳几乎察觉不到;但要是延迟累积到 500ms,通话就没法进行了。
所以 RTP 的核心哲学是 用可接受的丢包换取低延迟 。这个取舍贯穿了整个协议的设计。
1.2 RTP、RTCP、RTSP 三个东西别搞混
初学的时候特别容易把这三个缩写搅在一起,我列个表说清楚:
| 名称 | 全称 | 干什么的 | 传输层 |
|---|---|---|---|
| RTP | Real-time Transport Protocol | 承载音视频媒体数据 | 通常 UDP |
| RTCP | RTP Control Protocol | 传输质量反馈、统计信息 | 通常 UDP |
| RTSP | Real Time Streaming Protocol | 控制流媒体会话(播放/暂停/seek) | 通常 TCP |
RTP 和 RTCP 是一对,通常一起出现。RTP 负责搬数据,RTCP 负责回报“我收到了多少、丢了多少、抖动多大”。发送方根据 RTCP 的反馈来调整码率、分辨率,这就是很多直播软件里“自适应码率”的底层依据。
RTSP 则是另一个层面的东西,它是信令协议,管的是“开始播”“暂停”“跳到第 30 秒”这类控制指令。你可以把 RTSP 理解成遥控器,RTP 理解成电视信号本身。监控摄像头领域 RTSP 用得特别多,你输入一个
rtsp://
开头的地址就能拉到摄像头的实时画面,背后就是 RTSP 协商 + RTP 传输。
1.3 一个 RTP 包长什么样
RTP 的固定头部是 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 |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
V(Version,2 bit) :版本号,目前固定是 2。看到不是 2 的包基本可以判定是脏数据。
P(Padding,1 bit) :填充标志。如果置 1,说明包尾有填充字节,最后一个字节的值告诉你填充了多少。加密场景下用得多,因为块加密要求数据长度对齐。
X(Extension,1 bit) :扩展标志。置 1 说明固定头后面还跟了一个扩展头。WebRTC 大量使用这个字段,比如把音频电平、视频旋转角度这些信息塞进扩展里。
CC(CSRC Count,4 bit) :CSRC 的数量。CSRC 是“贡献源”,用在混音场景。比如一个会议里三个人同时说话,服务器混音后发给第四个人,这个混音包的 SSRC 是服务器的,但 CSRC 列表里会列出那三个人的 SSRC。
M(Marker,1 bit) :标记位。它的含义由具体的负载类型决定。对视频来说,通常标记一帧的最后一个包;对音频来说,标记一段静音后的第一个包。这个位很关键,接收方靠它来判断帧边界。
PT(Payload Type,7 bit) :负载类型,决定后面的数据是什么编码。常见的值:0 是 PCMU(G.711 μ-law),8 是 PCMA,96-127 是动态分配,比如 96 可能是 H.264,111 可能是 Opus。具体对应关系靠 SDP 协商确定,不是写死的。
sequence number(16 bit) :序列号,每发一个包加 1,循环使用。接收方靠它检测丢包和乱序。注意它是在 发送时 递增的,不是按时间递增。
timestamp(32 bit) :时间戳,这是 RTP 最精妙的设计之一。它不是墙上时钟,而是 采样时刻 。对音频来说,如果采样率是 8000Hz,每个包装 160 个采样点(20ms),那时间戳每次就加 160。接收方拿到时间戳,就能算出这个包应该在什么时刻播放,从而实现平滑播放和音视频同步。
SSRC(32 bit) :同步源标识,随机生成,用来区分不同的媒体流。一个会话里每个流有唯一的 SSRC。如果发生冲突(两个源随机到了同一个值),协议规定了重新选择的机制。
这 12 个字节是理解 RTP 的基础,后面讲的所有应用场景,本质上都是在这 12 个字节上做文章。
2. 时间戳和序列号:RTP 抗抖动的两把钥匙
2.1 序列号解决“顺序和丢失”
网络传输中,包到达的顺序和发送顺序往往不一致,这叫乱序。UDP 不保证顺序,所以接收方必须自己排。序列号就是干这个的。
接收方维护一个期望序列号,每收到一个包就比对。如果收到的序列号比期望的大 1,正常;如果大很多,说明中间丢了包;如果比期望的小,说明是迟到的乱序包。这里有个细节:序列号是 16 位的,会从 65535 回绕到 0,所以比较的时候不能用简单的减法,得用模运算。RFC 1889 里给了一个判断“新包还是旧包”的算法,核心思路是把差值映射到 -32768 到 32767 的区间里再比较。
丢包检测出来之后怎么办?这就涉及具体的编解码器了。音频通常用 PLC(Packet Loss Concealment,丢包隐藏)技术,根据前后包的数据“猜”出一个替代波形,听起来不会断。视频则复杂得多,因为视频帧之间有依赖关系,丢一个 I 帧后面的 P 帧全废。所以视频通常配合 FEC(前向纠错)或者 NACK(重传请求)来补救。
2.2 时间戳解决“什么时候播”
序列号管顺序,时间戳管节奏。这两个是分开的,这点很多人一开始想不通。
假设你发一段音频,采样率 48000Hz,每 20ms 打一个包,每个包 960 个采样点。那么时间戳序列就是 0, 960, 1920, 2880……接收方收到这些包,知道每个包代表 20ms 的音频,按时间戳顺序播放就行。
但网络抖动会让包到达的时间不均匀:可能前三个包挤在一起到了,第四个包晚了 50ms。如果收到就播,声音就会忽快忽慢。所以接收方会设置一个 抖动缓冲区(Jitter Buffer) ,先把包攒一会儿,再按时间戳匀速吐出来。缓冲区大小是个权衡:太小,抖动大的时候会断音;太大,延迟高。一般语音通话的抖动缓冲区在 20ms 到 200ms 之间动态调整。
时间戳还有一个关键作用: 音视频同步 。音频和视频是两条独立的 RTP 流,各有各的 SSRC 和时间戳。它们的时间戳基准不一样,怎么对齐?靠 RTCP 里的 SR(Sender Report)包。SR 包里同时包含 RTP 时间戳和 NTP 墙上时间,接收方通过这个映射关系,把两条流都换算到同一个时间轴上,就能实现唇音同步。
2.3 抖动缓冲区的实际调参经验
我在实际项目里调过不少抖动缓冲区,分享几个踩过的坑。
第一个坑是 初始缓冲区设太小 。默认 50ms 看起来合理,但如果你的用户网络质量差,前几秒会疯狂断音。后来我改成启动时先给 100ms,然后根据 RTCP 反馈的抖动统计慢慢往下调。用户感知上,通话刚开始稍微延迟一点,比一上来就卡顿要好得多。
第二个坑是 自适应算法过于激进 。有些开源实现看到抖动小了就立刻把缓冲区缩到最小,结果网络稍微一波动又卡。我的做法是加一个“迟滞”机制:缩容要连续 N 个统计周期都稳定才执行,扩容则立即执行。这样对突发抖动反应快,对稳定网络又能省延迟。
第三个坑是 没考虑设备性能 。在低端设备上,抖动缓冲区太大反而导致 CPU 处理不过来,因为要同时维护大量待播放的包。这种场景下宁可牺牲一点音质,把缓冲区控制在 150ms 以内。
3. RTP 和 SIP 怎么配合:一次完整通话的拆解
3.1 分工:SIP 管“约”,RTP 管“聊”
SIP(Session Initiation Protocol)是信令协议,负责“建立、修改、结束”会话。RTP 负责会话建立之后的媒体传输。用打电话类比:SIP 是你拨号、对方接听、你们确认用什么语言交流的过程;RTP 是你们真正说话的声音。
一次典型的 SIP 通话流程是这样的:
- 主叫发 INVITE,里面带 SDP,描述自己支持哪些编码、RTP 端口是多少
- 被叫回 200 OK,里面也带 SDP,确认用哪个编码、自己的 RTP 端口
- 主叫回 ACK,握手完成
- 双方开始用协商好的参数互发 RTP 包
- 通话结束,一方发 BYE
关键在 SDP(Session Description Protocol)。它是 SIP 消息体里的一段文本,格式大概长这样:
v=0
o=- 123456 123456 IN IP4 192.168.1.100
s=-
c=IN IP4 192.168.1.100
t=0 0
m=audio 49170 RTP/AVP 0 8 96
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:96 opus/48000/2
m=audio 49170 RTP/AVP 0 8 96
这行是核心:媒体类型是 audio,RTP 端口 49170,传输协议 RTP/AVP,支持的负载类型是 0、8、96。下面的
a=rtpmap
行把负载类型映射到具体编码。双方各自列出自己支持的,然后取交集。
3.2 协商失败最常见的三个原因
我处理过的 SIP 通话问题里,媒体协商失败占了很大比例。总结下来主要是三个原因。
第一个是编码不匹配 。一方只支持 G.711,另一方只支持 Opus,没有交集,通话建立不起来。解决办法是在 SDP 里多列几种编码,按优先级排序,让对端有得选。但也不能列太多,有些老设备看到不认识的编码会直接拒绝整个 SDP。
第二个是 IP 地址填错
。SDP 里的
c=
行和
o=
行都要填 IP。如果设备有多张网卡,很容易填成内网地址,导致对端发过来的 RTP 包路由不到。我见过一个案例,服务器有公网和内网两个 IP,SIP 信令走公网,但 SDP 里填了内网 IP,结果信令通了,媒体一直单向。排查了半天才发现是 SDP 的问题。
第三个是端口被防火墙挡了 。RTP 用的端口范围通常是 16384-32767,很多企业防火墙默认只开 80、443、5060。SIP 信令走 5060 能通,但 RTP 端口没开,就变成“能打通但听不到声音”。这个问题的典型症状是:通话建立成功,但双方都听不到对方。解决办法要么是让防火墙放行 RTP 端口段,要么用 TURN 之类的中继方案。
3.3 一个真实的排查案例
之前有个客户反馈,说他们的软电话在办公室能用,回家就用不了。我让他抓了个包发过来,一看就明白了。
在办公室,客户端和服务器在同一个网段,SDP 里填的内网 IP 直接可达,RTP 包正常收发。回到家,客户端通过公网连服务器,SDP 里填的还是内网 IP(因为客户端不知道自己被 NAT 了),服务器按这个 IP 发 RTP,自然发不到。但 SIP 信令为什么能通?因为 SIP 服务器通常有 NAT 穿透机制,或者客户端配置了 outbound proxy,信令走了服务器中转。
这个问题的标准解法是 NAT 穿透,常见的有 STUN、TURN、ICE 三种。STUN 让客户端发现自己的公网映射地址,填到 SDP 里;TURN 在 STUN 失败时用中继服务器转发;ICE 则是把各种候选地址都列出来,逐个尝试连通性,选最优的。WebRTC 把这套东西集成得很好,但传统 SIP 电话需要单独配置。
4. 从零实现一个最小的 RTP 收发程序
4.1 为什么建议自己动手写一遍
看再多 RFC,不如自己写一个能跑的最小实现。因为 RTP 的很多细节,只有在你亲手拼字节、算时间戳的时候才会真正理解。比如序列号回绕、时间戳增量、SSRC 冲突处理,这些在文档里就是几句话,但实现的时候全是坑。
我用 Python 写一个最简版本,只做 G.711 音频的收发,不涉及 RTCP,目的是把 RTP 包的结构跑通。生产环境当然不能用这个,但用来学习足够了。
4.2 打包和解包的核心代码
先看打包。RTP 头是 12 字节,用
struct
模块拼:
import struct
import random
class RTPPacket:
def __init__(self):
self.version = 2
self.padding = 0
self.extension = 0
self.csrc_count = 0
self.marker = 0
self.payload_type = 0
self.sequence_number = 0
self.timestamp = 0
self.ssrc = random.randint(0, 0xFFFFFFFF)
self.payload = b''
def pack(self):
# 第一个字节:V(2) P(1) X(1) CC(4)
first_byte = (self.version << 6) | (self.padding << 5) | \
(self.extension << 4) | self.csrc_count
# 第二个字节:M(1) PT(7)
second_byte = (self.marker << 7) | self.payload_type
header = struct.pack('!BBHII',
first_byte,
second_byte,
self.sequence_number,
self.timestamp,
self.ssrc)
return header + self.payload
@classmethod
def unpack(cls, data):
if len(data) < 12:
raise ValueError('packet too short')
first_byte, second_byte, seq, ts, ssrc = struct.unpack('!BBHII', data[:12])
pkt = cls()
pkt.version = (first_byte >> 6) & 0x03
pkt.padding = (first_byte >> 5) & 0x01
pkt.extension = (first_byte >> 4) & 0x01
pkt.csrc_count = first_byte & 0x0F
pkt.marker = (second_byte >> 7) & 0x01
pkt.payload_type = second_byte & 0x7F
pkt.sequence_number = seq
pkt.timestamp = ts
pkt.ssrc = ssrc
# 跳过 CSRC 列表
offset = 12 + pkt.csrc_count * 4
pkt.payload = data[offset:]
return pkt
struct.pack('!BBHII', ...)
里的
!
表示网络字节序(大端),这是 RTP 规定的。
B
是无符号字节,
H
是无符号短整型(2 字节),
I
是无符号整型(4 字节)。拼出来的正好是 1+1+2+4+4=12 字节。
这里有个容易出错的地方:
位运算的优先级
。
self.version << 6
要先算,再和后面的做或运算。Python 里
<<
的优先级高于
|
,所以不用加括号也没问题,但为了可读性我还是加了。
4.3 发送端的节奏控制
打包只是第一步,真正难的是 按正确的节奏发 。音频要求每 20ms 发一个包,这个定时精度直接影响音质。
新手最容易犯的错是用
time.sleep(0.02)
来定时。这个精度太差了,操作系统调度一下就可能偏差几毫秒,累积起来就乱了。正确做法是用绝对时间计算下一次发送时刻:
import time
def send_loop(sock, dest, samples_per_packet=160, sample_rate=8000):
interval = samples_per_packet / sample_rate # 0.02 秒
seq = 0
ts = 0
next_send = time.time()
while running:
payload = get_audio_samples(samples_per_packet)
pkt = RTPPacket()
pkt.sequence_number = seq & 0xFFFF
pkt.timestamp = ts & 0xFFFFFFFF
pkt.payload_type = 0 # PCMU
pkt.payload = payload
sock.sendto(pkt.pack(), dest)
seq += 1
ts += samples_per_packet
next_send += interval
sleep_time = next_send - time.time()
if sleep_time > 0:
time.sleep(sleep_time)
else:
# 已经落后了,不睡,直接发下一个
next_send = time.time()
关键在
next_send += interval
这一行。它累加的是
理论时刻
,而不是从当前时间重新算。这样即使某次发送晚了,下一次也会尽量补回来,不会让误差累积。如果落后太多(
sleep_time
为负),就重置基准,避免疯狂追赶。
4.4 接收端的抖动缓冲
接收端收到包之后不能直接播,要先放进抖动缓冲区。最简单的实现是一个按时间戳排序的优先队列:
import heapq
class JitterBuffer:
def __init__(self, max_delay_ms=200):
self.buffer = []
self.max_delay = max_delay_ms / 1000.0
self.playout_ts = None
def push(self, pkt):
heapq.heappush(self.buffer, (pkt.timestamp, pkt))
def pop(self, now):
if not self.buffer:
return None
ts, pkt = self.buffer[0]
if self.playout_ts is None:
self.playout_ts = ts
# 判断是否到了播放时刻
if ts <= self.playout_ts:
heapq.heappop(self.buffer)
self.playout_ts += 160 # 假设每个包 160 采样点
return pkt
return None
这个实现很粗糙,但能说明原理。生产级的抖动缓冲区要处理丢包、乱序、时间戳回绕、动态调整深度,复杂度高得多。不过自己写一遍之后,再看开源实现就轻松多了。
5. 那个“rpgvx rtp is not found”到底是什么
5.1 和协议毫无关系的同名组件
搜 RTP 的时候,你大概率会看到一堆游戏报错的帖子。这个 RTP 和实时传输协议没有任何关系,它是 RPG Maker VX / VX Ace 运行库里的一个文件名,通常是
RTP.dll
或者类似的组件。
RPG Maker 是 Enterbrain(现 Gotcha Gotcha Games)出的游戏制作工具,很多独立游戏用它开发。用 RPG Maker 做的游戏,运行时依赖一套运行库,里面包含图形、音频、输入处理等模块。RTP 就是其中一个模块的缩写,具体含义官方没有明确说明,社区普遍认为是 Runtime Package 之类的意思。
报错“rpgvx rtp is not found”的意思是:游戏启动时找不到这个运行库文件。常见原因有三个:一是游戏文件不完整,安装或解压的时候漏了;二是杀毒软件误删,因为有些老版本的 DLL 会被误判;三是系统缺少运行环境,比如没装对应的 Visual C++ 运行库。
5.2 修复思路
修复方法其实不复杂,按顺序排查:
-
确认游戏目录下有没有
RTP.dll或类似文件。没有的话,从游戏原始压缩包里重新解压一份。 - 检查杀毒软件的隔离区,看是不是被误删了。如果是,加白名单后恢复。
- 安装对应的 Visual C++ Redistributable。RPG Maker VX 时代的游戏通常需要 VC++ 2005 或 2008 的运行库。
- 如果还是不行,去 RPG Maker 官方社区找对应版本的 RTP 包单独下载安装。
这里要提醒一句:网上有些所谓的“RTP 修复工具”捆绑了乱七八糟的东西,下载的时候留个心眼。优先从游戏官方渠道或者可信的社区资源获取。
5.3 为什么搜索引擎会把它们混在一起
这个现象挺有意思。RTP 这个缩写在音视频领域是标准术语,在游戏领域只是一个小众运行库的名字。但搜索引擎的关键词匹配是字面的,用户搜“RTP”的时候,两类内容都会出来。加上游戏报错的搜索量可能比协议文档还大,导致结果被游戏内容淹没。
如果你是想学协议,搜索的时候加上限定词,比如“RTP 协议 RFC”“RTP 头部格式”“RTP 和 SIP”,就能过滤掉大部分游戏内容。反过来,如果你是想修游戏,搜“rpgvx rtp not found 解决”比单搜“RTP”精准得多。
6. WebRTC 时代的 RTP:老协议的新生命
6.1 WebRTC 为什么还在用 RTP
WebRTC 是浏览器实时通信的标准,2011 年提出,现在几乎所有主流浏览器都支持。有意思的是,这个看起来很现代的技术,底层媒体传输用的还是 RTP。
原因很简单:RTP 经过几十年验证,设计足够灵活,能适配各种编码和网络条件。WebRTC 在 RTP 基础上做了几件事:一是用 SRTP(Secure RTP)加密,保证媒体流安全;二是用 RTCP 的扩展反馈机制(如 NACK、PLI、REMB)做拥塞控制;三是用 ICE 做 NAT 穿透。但核心的包格式、时间戳、序列号机制,和 RFC 3550 里定义的没有本质区别。
所以学 RTP 不是学一个过时的东西。你搞懂了 RTP,再去看 WebRTC 的媒体传输部分,会发现很多设计都是相通的。
6.2 SRTP 加密的几个关键点
SRTP 不是简单地把整个 RTP 包加密,而是只加密负载部分,头部保持明文。为什么?因为中间的网络设备(比如 SFU 服务器)需要读头部信息来做转发决策,比如根据 SSRC 判断这是哪条流,根据序列号做丢包统计。如果头部也加密了,这些设备就没法工作了。
SRTP 的密钥管理靠 DTLS-SRTP 握手。简单说,双方先通过 DTLS 协商出一对密钥,然后用这对密钥加密 RTP 流。DTLS 握手本身走的是另一个端口,和 RTP 分开。这个设计的好处是密钥协商和媒体传输解耦,各自可以独立优化。
实际部署中,SRTP 最常见的坑是 密钥不匹配 。症状是能收到包但解不开,日志里一堆解密失败。原因通常是 DTLS 握手没完成就急着发媒体,或者双方对加密套件的支持不一致。排查的时候先看 DTLS 握手日志,确认密钥协商成功,再看 SRTP 解密。
6.3 拥塞控制:RTP 之上的智能层
RTP 本身不做拥塞控制,它只是老老实实发包。但在 WebRTC 里,发送方需要根据网络状况动态调整码率,否则网络一拥塞就雪崩。这套机制建立在 RTCP 反馈之上。
核心思路是:接收方通过 RTCP 的 RR(Receiver Report)包告诉发送方丢包率、抖动、往返时延;发送方根据这些信息估算可用带宽,调整编码码率。Google 的 GCC(Google Congestion Control)算法是这方面的代表,后来演进成了 transport-cc 机制,能提供更细粒度的反馈。
我在调 WebRTC 的时候发现,拥塞控制参数不能照搬默认值。比如在移动网络下,默认的探测策略太激进,容易把带宽估高,导致丢包。后来我把起始码率调低,探测步长调小,稳定性好了很多。这类参数没有万能值,得根据实际网络环境调。
7. 实际项目里 RTP 的常见故障和处理
7.1 单向音频:先查 SDP 再查防火墙
单向音频是最常见的故障之一,症状是一方能听到另一方,反过来不行。排查顺序我总结成三步。
第一步,看 SDP。双方交换的 SDP 里,
c=
行和
m=
行的 IP 端口是否正确。特别是有 NAT 的场景,SDP 里填的可能是内网地址。抓包看 RTP 实际发往哪个地址,和 SDP 里声明的是否一致。
第二步,看防火墙。RTP 端口通常是动态分配的,如果防火墙只放行了信令端口,媒体包会被丢。用
tcpdump
或 Wireshark 在两端抓包,看包有没有到达对端网卡。如果发送端有、接收端没有,基本就是中间被挡了。
第三步,看 SSRC 和 PT。如果包到了但解不出来,可能是 SSRC 冲突或者 PT 不匹配。SSRC 冲突的概率很低,但 PT 不匹配很常见,尤其是双方对动态 PT 的映射理解不一致的时候。
7.2 音频断续:抖动缓冲区背锅还是网络背锅
音频断续的原因很多,得先定位是网络问题还是本地问题。
一个简单的判断方法:看 RTCP 的丢包统计。如果丢包率超过 5%,基本是网络问题,得从链路质量或者码率入手。如果丢包率很低但还断续,那多半是抖动缓冲区的问题,可能是缓冲区太小,或者自适应算法没跟上。
还有一种情况是 本地 CPU 过载 。音频编码虽然不重,但如果设备同时在跑视频编码、渲染、网络收发,CPU 打满的时候音频线程抢不到时间片,也会断续。这种问题的特征是:网络统计正常,但本地日志里有大量处理超时。解决办法是给音频线程提高优先级,或者降低视频编码复杂度。
7.3 时间戳跳变:一个容易被忽略的坑
时间戳应该是单调递增的,但实际中会遇到跳变。常见原因有两个。
一个是 编码器重启 。如果编码器因为某种原因重新初始化,时间戳可能从 0 重新开始。接收方看到时间戳突然变小,会以为是乱序包,处理逻辑就乱了。解决办法是在编码器重启时发一个特殊的标记,或者用 RTCP 的 SR 包重新同步。
另一个是 采样率切换 。有些场景下会根据网络状况动态切换采样率,比如从 48kHz 降到 16kHz。这时候时间戳的增量会变,接收方如果还用旧的采样率计算播放时刻,就会出错。正确做法是在切换时通过信令通知对端,或者用 SDP 重新协商。
我在一个项目里遇到过时间戳跳变导致音频加速的问题,排查了很久才发现是编码器在特定条件下会重置。后来加了一个检测逻辑:如果时间戳变化超过阈值,就触发重新同步,问题就解决了。
8. 写在最后的一些个人体会
RTP 这个协议,刚看的时候觉得字段不多,应该不难。真正用起来才发现,每个字段背后都有一堆实际场景要考虑。序列号回绕、时间戳同步、SSRC 冲突、抖动缓冲、NAT 穿透,这些东西文档里都有,但只有踩过坑才知道哪个参数该调、哪个边界条件容易出问题。
我的建议是,如果你在做音视频相关的东西,一定要自己抓包看 RTP 流。Wireshark 能直接解析 RTP,把序列号、时间戳、SSRC 都列出来,还能画抖动曲线。看几次真实的包,比读十遍 RFC 都管用。
另外,别被那些同名的东西干扰。搜资料的时候加限定词,能省很多时间。RTP 在音视频领域是个成熟稳定的协议,值得花时间搞透。
更多推荐
所有评论(0)