1. 流媒体基础与RTP协议核心

各位同学,咱们今天聊聊流媒体传输的根基——RTP协议。说实话,我做了十几年流媒体开发,见过太多人一上来就调FFmpeg参数、改编码器配置,结果延迟降不下来、卡顿解决不了。为什么?因为底层协议没吃透。

这一章,咱们就把地基打牢。我会从传输模型讲起,再到RTP/RTCP协议栈,最后深入时间戳和SSRC这两个最容易踩坑的地方。

1.1 流媒体传输模型:从采集到播放

先看一张整体架构图,你心里就有谱了。

流媒体端到端传输模型 采集 编码 RTP封装 网络传输 网络接收 RTP解封装 解码 渲染 💡 关键点: RTP封装是连接编码器和网络传输的桥梁。编码器输出的是压缩后的码流(如H.264 NAL单元), RTP负责把这些码流切成合适大小的包,加上时间戳和序号,扔进网络。

这个模型其实不复杂。左边是发送端,右边是接收端。中间那个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:流的身份证,注意防冲突