拉 UDP 时,接收端必须在发之前就把端口绑好。 顺序反了就是一帧都没有——而且全程不报错。
8.4.2 站在发送端,这一节换到接收端。 同一个协议,两种角色踩的坑完全不一样。
一、先开接收端,再发
这一节的实验都靠一个中间转发器:A 端口收、B 端口转,中间可以按规则丢包。 接收端挂在 B 上,发送端往 A 打,这样才方便「制造故障」。
先绑端口再发,解出 23 帧,一次成功。 成功率不是必然的——它取决于接收端有没有在发之前把 socket 建好。
二、顺序反了,一帧都没有
把顺序调过来:先把整段发完,再开接收端。
顺序 解出帧
先开接收端,再发 23 先发完,再开接收端 0
一帧都没有,而且不报错、不警告,看起来就像「流本来就没发过来」。 UDP 不排队、不缓存——没人接的那一刻,包就是没了。
这条对命令行同样成立:ffplay 要先起,ffmpeg 推流再跟上。
反过来写,ffplay 起来时对面已经推完了,只能干等着。
三、中间丢掉一部分包
转发器上加一条规则:每 N 个包扔掉 1 个。
丢包规则 转发包 扔掉包 解出帧
不丢 15 0 21 每 20 包丢 1 15 0 21 每 5 包丢 1 12 3 0
第二行值得单独说:「每 20 包丢 1」的扔掉包数是 0。 因为整段只发了 15 个包,20 包一丢的规则一次都没轮到。 这不是规则失效,是样本太少——做丢包实验时最容易忽略的一点。
第三行只丢 3 个包,帧数就从 21 掉到 0。 为什么这么脆?因为解复用器要先读到 PAT 与 PMT 表, 这两张表一丢,后面所有 TS 包都成了「不知道属于哪一路」的垃圾。
所以工程上真正用的 UDP 直播都不是裸 UDP: 要么 RTP 骑在 UDP 上(有序号),要么 SRT 自己带重传与纠错。
四、命令行拉 UDP
FFmpeg 家族拉 UDP 只要一条命令:
ffplay -f mpegts udp://127.0.0.1:5004?fifo_size=1000000
-f mpegts 是必须的:UDP 里没有任何「我是什么格式」的信息,
不像 HTTP 有 Content-Type、也不像文件有扩展名,只能自己说。
fifo_size 是接收缓冲,调大能抗一点抖动,但会加大延迟。
overrun_nonfatal=1 建议一起带上,缓冲溢出时只丢数据、不让进程退出。
再补一个常用参数:timeout 控制读超时(微秒)。
不设的话,对面不发东西时会一直挂着,看起来像「程序卡死」。
五、动手 Demo:顺序、丢包、解码,三件事一次量完
下面这段先验「顺序」,再用转发器造三档丢包,最后看解出多少帧。
关键是接收端要用 fifo_size 加大缓冲,否则本地回环也会溢。
import io, socket, threading, time
from fractions import Fraction
import av
def free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
def ts_bytes(n=25):
u"""把 s7.mp4 的前 n 帧封成 TS,返回全部字节。"""
buf = io.BytesIO()
o = av.open(buf, "w", format="mpegts")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
c, k = av.open("s7.mp4"), 0
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
k += 1
if k >= n:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
return buf.getvalue()
def pull(port, cap=40):
st = {"n": 0, "err": None}
def go():
try:
c = av.open("udp://127.0.0.1:%d?fifo_size=1000000"
"&overrun_nonfatal=1" % port,
options={"timeout": "8000000"})
for _f in c.decode(video=0):
st["n"] += 1
if st["n"] >= cap:
break
c.close()
except Exception as e:
st["err"] = type(e).__name__
th = threading.Thread(target=go)
th.daemon = True
th.start()
return th, st
TS = ts_bytes()
chunks = [TS[i:i + 1316] for i in range(0, len(TS), 1316)]
print(u"TS 共 %d 字节,切成 %d 个 UDP 包" % (len(TS), len(chunks)))
# ① 先开接收端
p = free_port()
th, st = pull(p)
time.sleep(1.5)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for ch in chunks:
tx.sendto(ch, ("127.0.0.1", p))
time.sleep(0.002)
tx.close()
th.join(9.0)
print(u"先开接收端:%d 帧" % st["n"])
# ② 先发完再开接收端
p2 = free_port()
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for ch in chunks:
tx.sendto(ch, ("127.0.0.1", p2))
time.sleep(0.002)
tx.close()
time.sleep(0.2)
th2, st2 = pull(p2)
th2.join(9.0)
print(u"先发完再开:%d 帧" % st2["n"])
实跑输出:
--------------------------------------------------------------------
8.4.3 FFmpeg 实现 UDP 直播拉流
--------------------------------------------------------------------
**① 先开接收端,再发:能解出帧**
**先绑端口再发,解出 23 帧。**
**② 先发完,再开接收端:一帧都没有**
顺序 解出帧
---------------------------------
先开接收端,再发 23
先发完,再开接收端 0
**顺序反了就是一帧都没有,而且不报错。**
**UDP 不排队、不缓存——没人接的那一刻,包就是没了。**
**③ 在中间丢掉一部分 TS 包**
丢包规则 转发包 扔掉包 解出帧
-------------------------------------------------
不丢 15 0 21
每 20 包丢 1 15 0 21
每 5 包丢 1 12 3 0
**这次丢的是 TS 包,不是 RTP 包——连「少了哪个」都不知道。**
**接收端只能靠 TS 自己的连续性计数器(占 4 位)去猜,猜到了也没法补。**
**所以工程上真正要用的 UDP 直播都不是裸 UDP:**
**要么 RTP 骑在 UDP 上(有序号),要么 SRT 自己带重传与纠错。**
**④ 命令行拉 UDP**
`ffplay -f mpegts udp://127.0.0.1:5004?fifo_size=1000000`
**`-f mpegts` 是必须的:UDP 里没有「我是什么格式」的信息,得自己说。**
**`fifo_size` 是接收缓冲,调大能抗一点抖动,但会加大延迟。**
第一段能解出帧,第二段是 0——顺序就是全部。
丢包那段用的是转发器:A 收、B 转,k % 20 == 0 时直接 continue。
整段只有 15 个包,所以「每 20 包丢 1」一次都没触发——这一行最能说明样本量的重要。
30 秒实验
- 把
fifo_size去掉:看本地回环会不会因为缓冲太小而丢包。 - 把
overrun_nonfatal=1去掉:缓冲溢出时看进程是不是直接退出。 - 把发送间隔从 2 毫秒改成 20 毫秒:看接收端还能不能追上。
常见误区
- 先跑发送端、再跑
ffplay。UDP 没有缓存,发完就没了。 - 不写
-f mpegts。UDP 不携带格式信息,解复用器会乱猜。 - 以为「丢几个包只是花屏」。丢的若是 PAT/PMT,整条流直接解不出。
自测三问(点开对答案)
1. 为什么先发完再开接收端会一帧都没有? —— UDP 不缓存,没人在监听时包被内核直接丢弃,之后也不会补。 2. 「每 20 包丢 1」为什么扔掉包数是 0? —— 整段只发了 15 个包,20 包一丢的规则一次都没轮到。 3. 为什么丢 3 个包就能让 21 帧变成 0 帧? —— PAT 与 PMT 表一丢,后面的 TS 包就不知道该属于哪一路。- ☐ 8.4.3 完,下一节 8.5 流媒体服务器的搭建