1. 流媒体基础与RTP协议核心
各位同学,咱们今天聊聊流媒体传输的根基——RTP协议。说实话,我做了十几年流媒体开发,见过太多人一上来就调FFmpeg参数、改编码器配置,结果延迟降不下来、卡顿解决不了。为什么?因为底层协议没吃透。
这一章,咱们就把地基打牢。我会从传输模型讲起,再到RTP/RTCP协议栈,最后深入时间戳和SSRC这两个最容易踩坑的地方。
1.1 流媒体传输模型:从采集到播放
先看一张整体架构图,你心里就有谱了。
这个模型其实不复杂。左边是发送端,右边是接收端。中间那个RTP封装/解封装,就是咱们这章的重点。
我个人习惯把流媒体系统分成三层:
- 媒体层:原始音视频数据,比如PCM音频、YUV视频帧
- 编码层:压缩后的码流,比如H.264、AAC
- 传输层:RTP/UDP/IP,负责把数据从A点搬到B点
很多新手只盯着编码层调参数,忽略了传输层。你想想看,编码再好,RTP包丢了、乱序了、时间戳对不上,播放端照样花屏、卡顿、音画不同步。
1.2 RTP/RTCP协议栈:一对好搭档
RTP和RTCP,就像一对双胞胎。RTP负责运数据,RTCP负责反馈状态。
| 协议 | 端口 | 方向 | 主要职责 |
|---|---|---|---|
| RTP | 偶数端口(如5004) | 发送端→接收端 | 承载音视频数据,提供时间戳、序号、SSRC |
| RTCP | 奇数端口(如5005) | 双向 | 报告丢包率、抖动、往返时延,同步多个流 |
这里有个坑,我刚开始做项目时踩过——RTP和RTCP必须用相邻的端口对。很多防火墙只开了RTP端口,忘了开RTCP,结果接收端收不到反馈报告,发送端也不知道网络状况,一直按最高码率发,丢包率飙升。
⚠️ 避坑指南:我曾经在一个视频会议项目中,发现接收端画面频繁卡顿。排查了两天,最后发现是RTCP端口被防火墙拦截了。发送端收不到RR(Receiver Report),以为网络很好,一直以8Mbps码率发,实际带宽只有2Mbps。解决办法:确保RTP/RTCP端口对同时开放,或者在应用层做带宽探测兜底。
1.3 RTP包头结构:12字节的玄机
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。目前主流就是RTPv2。
- P (Padding):填充位。有些加密算法要求块对齐,会用到这个。
- X (Extension):扩展位。需要传额外信息时置1,比如WebRTC的abs-send-time。
- CC (CSRC Count):CSRC个数。混流场景用,比如多方通话。
- M (Marker):标记位。视频帧的结束、音频帧的开始,常用这个标识。
- PT (Payload Type):负载类型。告诉接收端这个包是H.264、AAC还是别的。
- Sequence Number:序号。检测丢包、排序用。
- Timestamp:时间戳。播放同步的核心。
- SSRC:同步源标识。每个流唯一。
💡 我的经验:PT字段在RFC中定义了静态映射,比如PT=96通常用于H.264。但实际项目中,我建议用动态PT(96-127),避免跟标准冲突。有一次我用了PT=34(H.263),结果某个老版本播放器直接不认,折腾了半天才发现是PT值撞车了。
1.4 时间戳机制:音画同步的灵魂
时间戳是RTP里最容易出问题的地方。说白了,它就是告诉接收端:这个包应该在什么时候播放。
但这里有个关键点——RTP时间戳的时钟频率跟采样率挂钩,不是用毫秒。
| 媒体类型 | 典型采样率 | 时间戳增量(每帧) | 说明 |
|---|---|---|---|
| PCM音频 | 8000 Hz | 160(20ms帧) | 每20ms增加160 |
| AAC音频 | 44100 Hz | 1024(每帧采样数) | 每帧增加1024 |
| H.264视频 | 90000 Hz | 3000(33ms帧) | 固定90kHz时钟 |
为什么视频要用90000 Hz?因为视频帧率可能是29.97、30、60这些非整数,90k能被这些帧率整除,保证时间戳精度。我见过有人直接用毫秒做视频时间戳,结果30fps的视频每帧增量33.333...毫秒,取整后累积误差越来越大,半小时后音画差了好几秒。
🔑 核心原则:音频时间戳用采样率做时钟,视频时间戳用90kHz做时钟。不要混用,不要自己发明时钟频率。
1.5 SSRC机制:流的身份证
SSRC(Synchronization Source)是每个RTP流的唯一标识。接收端靠它区分不同的流。
SSRC是32位随机数,生成时有个原则——尽量保证全局唯一。但随机嘛,总有撞车的时候。
我记得有一次做直播测试,推了两路流到同一个服务器,结果SSRC冲突了。接收端把两路流的包混在一起,画面变成「鬼影」——两路视频叠加显示。排查过程很痛苦,最后在Wireshark里发现两个流的SSRC一模一样。
SSRC冲突怎么处理?RTCP的SDES(Source Description)报文里有个CNAME(Canonical Name),可以作为辅助标识。但更靠谱的做法是:
- 生成SSRC时用高质量的随机数(比如/dev/urandom)
- 应用层维护SSRC分配表,避免冲突
- 收到RTCP的BYE报文后,释放对应的SSRC
⚠️ 注意:SSRC冲突检测是接收端的责任。RFC 3550规定,接收端如果发现两个流SSRC相同,应该发送RTCP的BYE报文通知发送端。但很多实现偷懒没做这个检测。我的建议是:在接收端主动做SSRC去重,不要依赖发送端。
1.6 实战:用FFmpeg抓一个RTP流看看
光说不练假把式。咱们用FFmpeg抓个RTP流,看看实际包长什么样。
先启动一个RTP发送端:
ffmpeg -re -i input.mp4 -c copy -f rtp rtp://127.0.0.1:5004
然后在另一个终端用Wireshark抓包,或者直接用FFmpeg的sdp文件分析。FFmpeg会生成一个SDP文件,里面包含了RTP的配置信息:
v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 127.0.0.1
t=0 0
a=tool:libavformat 58.76.100
m=video 5004 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1
看到没?PT=96,映射到H.264,时钟频率90000Hz。这就是FFmpeg默认的RTP封装方式。
我个人习惯在调试时加个-sdp_file output.sdp参数,把SDP导出来,方便播放器直接打开:
ffmpeg -re -i input.mp4 -c copy -f rtp -sdp_file output.sdp rtp://127.0.0.1:5004
然后用VLC打开这个SDP文件,就能看到实时流了。这是最快速的RTP调试方法。
1.7 本章小结
这一章咱们把流媒体的地基打了一遍:
- 传输模型:采集→编码→RTP封装→网络→RTP解封装→解码→渲染
- RTP/RTCP:一个运数据,一个做反馈,端口成对出现
- 时间戳:音频用采样率,视频用90kHz,别混用
- SSRC:流的身份证,注意防冲突