实时音视频要的不是「文件变小」,是「每一秒都有东西可播」。 所以它跑在 UDP 上自己搭一套:RTP 打包、抖动缓冲、丢包隐藏。
前面所有应用协议都有一个共同前提:数据必须完整。这一节是全书第一个允许丢包的场景。 Demo 把包头开销算成表,看为什么「压低码率省带宽」在小包上会失效。
一、三个和文件传输完全不同的要求
传文件的目标是「每个字节都对」,慢一点没关系。 实时媒体的目标是「每一秒都有东西可播」,宁可少一个画面,也不能卡住等。 这两个目标在丢包那一瞬间直接冲突:等重传,画面就卡住了;不等,画面就糊一下。 所有实时协议的设计,都是围绕这个取舍展开的。
| 维度 | 文件传输 | 实时媒体 |
|---|---|---|
| 目标 | 每个字节都到 | 每一秒都有东西可播 |
| 丢包 | 重传,等到补齐 | 丢掉,接着播下一帧 |
| 顺序 | 必须有序 | 可以乱序到达,按时间戳排 |
| 时延 | 不敏感 | 超过 150 毫秒就开始难受 |
| 传输层 | TCP | UDP 加 RTP |
第三行是很多人第一次听会愣一下的地方:实时媒体允许乱序。 因为每个包都带时间戳,接收端可以按时间戳重新排队, 晚到 20 毫秒的包,只要还在缓冲区里就还能用。 这就把「顺序」这个 TCP 最擅长的活儿,变成了应用层自己的事。 第四行的数字要记住:人对时延的容忍在 150 毫秒左右, 超过 400 毫秒就会开始「抢话」——你说完了对方才听到,于是两个人同时开口。 这不是网络问题,是人的问题,任何协议都绕不过去。
二、RTP:给每个包贴时间戳
RTP 是实时传输协议,它的头部只有 12 字节,装四样东西: 负载类型(是语音还是视频、用什么编码)、序号、时间戳、同步源标识。 序号解决「丢没丢」:连续两个包序号差 3,那就是丢了两个, 接收端据此算丢包率,也据此决定要不要做补偿。 时间戳解决「什么时候播」:它不是墙上时间,而是按采样率递增的计数, 所以接收端能算出两个包的间隔,从而知道该等多久。 配套的还有 RTCP,它每几秒发一次统计报告:收了多少、丢了多少、抖动多大。 发送端拿到这些数字,才知道该降码率还是该加冗余。 RTP 负责送,RTCP 负责反馈,两个合起来才是一套完整的东西。 固定的开销是 40 字节:RTP 12、UDP 8、IP 20。 这 40 字节和负载大小无关,每个包都要付一次。 于是就有了下面这个反直觉的结论。
三、头开销和打包间隔是对立的
一路语音打成 RTP 包,头占比是可以算出来的:
| 编码 | 码率 | 负载字节 | 整包字节 | 头占比 |
|---|---|---|---|---|
| G.711 | 64 kbps | 160 | 200 | 20% |
| Opus | 32 kbps | 80 | 120 | 33% |
| G.729 | 8 kbps | 20 | 60 | 67% |
第三行是关键:码率压到 8 kbps,头上却占了 67%。 因为你把负载从 160 字节压到 20 字节,省了 87.5%,整包却只从 200 降到 60,省了 70%。 「压缩省带宽」这件事,在小包上会被固定的头吃掉一大半。 另一条路是把包攒大:同样是 G.729,从 20 毫秒改成 60 毫秒一个包, 头占比从 67% 降到 40%,代价是延迟多了 40 毫秒。 这就是为什么 20 毫秒成了业界默认值——它是「延迟」和「带宽」两条曲线的交点附近。 还有一个更极端的例子:如果 5 毫秒打一个包,每秒要发 200 个包, 每个包都带走 40 字节的头,一路 64 kbps 的语音实际要占 128 kbps,正好是标称的两倍。 所以「包越小延迟越低」这句话,只在带宽不要钱的时候成立。
四、动手 Demo:实时音视频为什么必须在 UDP 上自己搭一套
把几种编码的包长、包率、头占比算出来,再算一遍 5 毫秒打包要多花多少带宽。
def pace(name, rate, ptime):
payload = rate // 8 * ptime // 1000
total = payload + 40
pps = 1000.0 / ptime
print(name, payload, total, round(pps, 1),
"%.0f%%" % (100.0 * 40 / total))
pace("G.711", 64000, 20)
pace("Opus", 32000, 20)
pace("G.729", 8000, 20)
pace("G.729 长打包", 8000, 60)
for ptime in (5, 10, 20, 40, 60, 120):
payload = 64000 // 8 * ptime // 1000
total = payload + 40
pps = 1000.0 / ptime
print(ptime, "ms", total, "B",
"%.1f kbps" % (total * 8 * pps / 1000.0))
实跑输出:
--------------------------------------------------------------------
实时音视频为什么必须在 UDP 上自己搭一套
--------------------------------------------------------------------
**一路语音打成 RTP 包,包长和包率是算出来的,不是猜的。**
**每个包固定多花 40 字节**:**RTP 12、UDP 8、IP 20。**
**下面把几种编码的开销摆在一起。**
编码 码率 打包间隔 负载字节 整包字节 包/秒 头占比
-------------------------------------------------------------------------------------
G.711 窄带 64 kbps 20 ms 160 200 50 20%
Opus 宽带 32 kbps 20 ms 80 120 50 33%
G.729 窄带 8 kbps 20 ms 20 60 50 67%
G.729 长打包 8 kbps 60 ms 60 100 17 40%
**第三行最关键**:**8 kbps 的 G.729,头上占了 67%。**
**因为包头是固定的 40 字节,码率越低,它占比越高。**
**所以「压低码率省带宽」这件事在小包上会失效**——
**你把负载从 160 字节压到 20 字节(省 87.5%),整包只从 200 降到 60(省 70%)。**
**第四行给出另一条路:把包攒大一点。**
**同样是 G.729,60 毫秒一个包,头占比就从 67% 降到 40%。**
**代价是延迟多了 40 毫秒。**
**把「延迟」和「带宽」的取舍算成一张表**:
打包间隔 负载 整包 包/秒 实际占用带宽
------------------------------------------------------
5 ms 40 80 200.0 128.0 kbps
10 ms 80 120 100.0 96.0 kbps
20 ms 160 200 50.0 80.0 kbps
40 ms 320 360 25.0 72.0 kbps
60 ms 480 520 16.7 69.3 kbps
120 ms 960 1000 8.3 66.7 kbps
**第一行和第二行的带宽比标称的 64 kbps 高出一大截**:
**5 毫秒一包时要 128 kbps,正好是标称的两倍。**
**因为每秒要发 200 个包,每个包都要带走 40 字节的头。**
**所以「延迟」和「带宽」在这里是直接对立的**——**包越小延迟越低,但头开销越大。**
**20 毫秒是业界折中的结果。**
**为什么不用 TCP**:
维度 TCP 怎么做 实时媒体要什么
-----------------------------------------------------------------------------------
**重传** **TCP 会补,但要等一个 RTT** **补回来也过期了,不如丢掉**
**顺序** **TCP 保证有序,队头堵住就全等** **后到的先播,用户听不出来**
**拥塞控制** **TCP 一丢包就减半** **宁可降码率也不降平滑度**
**连接** **要握手,断了要重连** **UDP 无连接,换网无缝**
**四行的结论是同一个**:
**TCP 优化的是「每个字节都正确到达」,实时媒体优化的是「每一秒都有东西可播」。**
**这两个目标在丢包时直接冲突。**
**所以 RTP 干脆跑在 UDP 上,把「要不要重传」的决定权交给应用层。**
**WebRTC 就是这么做的,QUIC 后来也是这个思路。**
注意第一张表最后一行:G.729 长打包的负载从 20 涨到 60,头占比从 67% 掉到 40%, 而码率一点没变。 这就是「打包间隔」这个参数的全部意义——它不动编码,只动分组的颗粒度。 第二张表里最刺眼的是 5 毫秒那一行的 128 kbps:标称 64 kbps 的编码,实占两倍。 如果把这条流放进一条 100 kbps 的链路,它会直接把链路打满, 而你看配置时明明写着「64 kbps 编码」。 最后那张 TCP 对比表说的是同一件事: TCP 优化的是「每个字节都正确到达」,实时媒体优化的是「每一秒都有东西可播」。 丢包时这两个目标无法同时满足,所以只能不用 TCP。
30 秒实验
- 算一下:G.729、20 毫秒打包,头那部分占多少 kbps? (60 字节乘 50 包每秒乘 8,得 24 kbps,其中头占 16 kbps)
- 想一下:抖动缓冲设成 200 毫秒,代价是什么?
- 想一下:拥塞时为什么该降码率而不是重传?
常见误区
- 以为「实时媒体用 UDP 是因为 UDP 更快」。 UDP 不比 TCP 快,它只是不管你丢不丢。
- 以为「码率减半带宽就减半」。 小包上头占比会把收益吃掉,8 kbps 的编码实际占 24 kbps。
- 以为「丢包一定要补」。 补回来也过了播放时刻,补了等于没补。
自测三问(点开对答案)
1. RTP 头里「序号」和「时间戳」各解决什么? —— **序号查丢包,时间戳定播放时刻**,两者分工不重叠。 2. 那固定的 40 字节从哪来? —— **RTP 12 加 UDP 8 加 IP 20**,每个包都要付一次。 3. 为什么 20 毫秒成了默认打包间隔? —— **延迟和带宽两条曲线在这里交叉**,再小头开销暴涨,再大延迟难受。下一节看 P2P:它不改协议,只改「谁把内容发给谁」。
- ☐ (本节完,下一节 8.7.2 P2P)