裸 RTP 里换个编码器,解出帧数能从二十多掉到一帧——差的是 SDP。 再在中间插一个会丢包的转发器,就能量出 UDP 的真实脾气。
8.2.2 讲发送端怎么打包,这一节讲接收端怎么解、丢包会怎样。 两条结论都很难在书上看到:一条关于参数集,一条关于丢包的静默。
一、为什么换了编码器就解不出
同一个发送端、同一条链路,只把编码器从 mpeg4 换成 H.264:
编码器 接收端解出
mpeg4 22 帧 libx264 1 帧
二十二帧对一帧,而且两边都不报错。 原因不在传输,在「解码器不知道参数」。
H.264 解码必须先拿到 SPS 与 PPS 两个参数集。 裸 RTP 里没有 SDP,接收端既不知道载荷是什么编码,也拿不到这两个参数集。 第一帧能出来,是因为包里恰巧带着关键帧头;后面全废了。
对照 8.1.3:同一个 H.264,走 RTSP 能解出 10 帧。
差别就在 SDP 里那句 sprop-parameter-sets——
所以「收 RTP 必须配一份 SDP」不是习惯,是硬性前提。
mpeg4 为什么不受影响:它的头信息在每个关键帧里自带, 不需要额外的参数集,所以「没有说明书也能解」。 这也说明「协议能不能自解释」会直接影响部署的难度。
二、插一个会丢包的转发器
本地回环一个包都不丢,直接连两端看不出差别。 所以实验搭三段:发送端 → 转发器(按规则扔掉)→ 接收端。
转发器只做一件事:收到一个包,按规则决定转不转。 这次用「每 N 个扔 1 个」,扔掉的包直接不回。
丢包规则 转发包 扔掉包 解出帧
不丢 50 0 22 每 10 包丢 1 45 5 20 每 2 包丢 1 25 25 11
三行都没有报错,但帧数一路往下掉:22、20、11。 丢一半包,帧数也差不多掉一半——大致成比例。
注意「转发包」那一列:不丢的时候是 50 个包。 25 帧的 TS 打成 RTP 是 50 个包,一帧两个——这比 8.1 的 RTSP(一帧近三个)少, 因为 TS 已经把多帧打包在一起了。
三、静默失败为什么难查
UDP 丢包最坑的地方不是丢,是「不告诉你丢了」。 三行实验里没有任何一个异常,只有帧数在变。
现象 可能的原因
连上了但没画面 对方没发,或被防火墙拦 有画面但花屏 丢包,且丢的是关键数据 帧数比预期少 丢了整帧,播放端直接跳过 卡住不动 等关键帧,前面全废了
排查的顺序应该是:先确认「包有没有到」,再看「到了能不能解」。 包没到是网络问题,到了解不出是参数问题——这两类完全不同的活。
一个实用手段:在中间用 tcpdump 或 Wireshark 抓一段,
看 RTP 的序号是不是连着的。序号跳了就说明中间丢了,
序号连着但解不出,那就是参数或编码的问题。
四、命令行怎么收 RTP
收 RTP 有两种写法,推荐第一种:
ffplay -protocol_whitelist file,udp,rtp -i sdp.txt
写法 要不要 SDP 风险
-i sdp.txt 要 稳,参数齐全 -i rtp://地址:端口 不要 靠载荷类型猜,可能判错
SDP 里要写清端口、载荷类型、编码、时钟频率与参数集。
直接写 rtp:// 的话,libav 只能按载荷类型去查静态表——
静态表里 33 是 MPEG-TS,但 H.264 用什么编号是私定的,它就猜不到了。
-protocol_whitelist 不加会报「Protocol not on whitelist」。
这是 ffmpeg 的安全措施:默认不允许随便连外部协议,免得被恶意文件引去访问本机。
这条限制在播本地文件里嵌的 SDP 时最容易踩到。
看到这个报错,把 file,udp,rtp 三个都加进白名单就对了。
五、动手 Demo:两种编码器、三档丢包
下面这段代码先用两个编码器各发一遍,比较接收端解出帧数; 再在中间插一个转发器,按三档规则丢包,看帧数怎么掉。
from fractions import Fraction
import av, socket, threading, time
def recv_n(port, sec=8.0):
n = [0]
def go():
c = av.open("rtp://127.0.0.1:%d" % port,
options={"timeout": "9000000"})
for _f in c.decode(video=0):
n[0] += 1
if n[0] >= 40:
break
c.close()
th = threading.Thread(target=go)
th.daemon = True
th.start()
return th, n
def send(port, codec, src="s7.mp4", nframes=25):
o = av.open("rtp://127.0.0.1:%d?pkt_size=1400" % port,
"w", format="rtp_mpegts")
st = o.add_stream(codec, rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
st.bit_rate = 400000
c = av.open(src)
k = 0
for f in c.decode(video=0):
for p in st.encode(f):
o.mux(p)
k += 1
if k >= nframes:
break
for p in st.encode(None):
o.mux(p)
o.close()
c.close()
for codec in ("mpeg4", "libx264"):
th, n = recv_n(24200)
time.sleep(1.5)
send(24200, codec)
th.join(9.0)
print(u"%-8s 解出 %d 帧" % (codec, n[0]))
实跑输出:
--------------------------------------------------------------------
8.2.3 FFmpeg 实现 RTP 直播拉流
--------------------------------------------------------------------
**① 先量一件事:裸 RTP 里换编码器会怎样**
**同一个发送端,只换编码器,接收端的解出帧数差得离谱:**
编码器 解出帧
----------------------
mpeg4 22
libx264 1
**mpeg4 解出 22 帧,H.264 只有 1 帧,而且两边都不报错。**
**原因不在传输,在「解码器不知道参数」:**
**H.264 解码必须先拿到 SPS/PPS,裸 RTP 里没有 SDP,**
**接收端既不知道这是 H.264,也拿不到那两个参数集。**
**能出来那几帧是碰巧带着关键帧头,后面的全废了。**
**对照一下 8.1 那一节:同一个 H.264,走 RTSP 能解出 10 帧。**
**差别就在 SDP 里那句 `sprop-parameter-sets`——**
**所以「收 RTP 必须配一份 SDP」不是习惯,是硬性前提。**
**② 中间放一个「会丢包」的转发器,才量得出丢包的后果**
**直接连两端,本地回环一个包都不丢,看不出差别。**
**所以这一节搭三段:发送端 → 转发器(按规则扔掉)→ 接收端,用 mpeg4 跑。**
丢包规则 转发包 扔掉包 解出帧
-------------------------------------------------
不丢 50 0 22
每 10 包丢 1 45 5 20
每 2 包丢 1 25 25 11
**三行都没有报错,但帧数一路往下掉。这就是 UDP 的性格:**
**丢了不告诉你,只有画面自己在花屏、卡顿、马赛克。**
**③ RTP 的序号只负责「让你知道少了」,不负责补**
**包头里那两字节序号是连续的。接收端发现 7 后面来了 9,就知道 8 没了。**
**知道之后怎么办,是上层的事:**
**播放器去「等下一个 I 帧」,而 RTP 自己什么都不做。**
**所以直播里丢一个 I 帧最致命——后面的 P 帧全都参考它,**
**只能等下一个 I 帧重来一次。这就是 GOP 越短越抗丢包的原因。**
**④ 命令行拉 RTP**
`ffplay -protocol_whitelist file,udp,rtp -i sdp.txt`
**`-protocol_whitelist` 不加会报「Protocol not on whitelist」,**
**因为播放器默认不允许随便连外部协议。**
**sdp.txt 里就写 8.1.2 那种 SDP,把 `m=` 里的端口改成实际收流端口即可。**
两次实验只差编码器,帧数却是 22 对 1。 这就是「参数集」的分量:传输一点问题都没有,是解码器缺说明书。
顺带看一个细节:两个编码器的码率都设了 400 kbps, 但 H.264 编出来的包更少更大(压缩效率高),mpeg4 更多更小。 所以「包数」跟「帧数」不是一回事,别拿包数当帧数看。
转发器那段代码只多了一个 k % drop == 0 的判断,
就能把丢包率精确控制住——比在真实网络里等丢包可靠多了。
30 秒实验
- 把丢包率调到每 20 包丢 1:帧数掉得更少,说明丢得少能靠缓冲补回来。
- 先丢第一个包:关键帧丢了,后面的帧基本全废。
- 给 mpeg4 也配一份 SDP:看帧数会不会比裸 RTP 更多(会,探测更准)。
常见误区
- 以为 H.264 解不出是传输问题。是缺 SPS/PPS。
- 丢包后以为程序会报错。UDP 不报错,只是帧数变少。
- 拿包数当帧数。一帧可能好几个包,也可能几帧一个包。
自测三问(点开对答案)
1. 裸 RTP 里 H.264 为什么解不出? —— 缺少 SDP 里的 sprop-parameter-sets,解码器拿不到 SPS 与 PPS。 2. 丢一半包,帧数会掉多少? —— 实测大约掉一半(22 掉到 11),大致成比例。 3. 为什么要加 -protocol_whitelist? —— ffmpeg 默认不允许随便连外部协议,不加会报协议不在白名单。- ☐ 8.2 完,下一节 8.3.1 HTTP 简介