裸 UDP 的 pkt_size 就是数据报本身的大小,1316 正好装 7 个 TS 包。 但它没有序号,丢一个都不知道——所以直播几乎不用裸 UDP。
8.2 讲的是 RTP 骑在 UDP 上,这一节把 RTP 那层拿掉,看看裸 UDP 长什么样。 8.4.3 再用 FFmpeg 把发送端与接收端接起来。
一、1316 一包,正好 7 个 TS
先看参照物:把一段 25 帧的画面封成 TS,一共 18800 字节、100 个 TS 包。
项 值
TS 总字节 18800 TS 包数 100 理论 UDP 包数(1316 一包) 15
18800 除以 1316 是 14.29,向上取整就是 15 个包。 1316 = 7 乘 188,正好装 7 个 TS 包,一个字节都不浪费。 这个数字不是巧合,是 MPEG-TS over UDP 的老规矩。
下面真发一遍,只改 pkt_size,看包数和每包大小怎么动。
pkt_size UDP 包数 每包最大
1316 29 1316 1400 28 1400 2000 27 2000
三个数字要连起来看:pkt_size 越大,包数越少,每包越满。
2000 比 1316 只少两个包,说明这一小段数据本来就装不满几个包。
注意实发包数比参照的 15 多得多。 因为推送时重新编码了一遍,一秒钟的画面编出来约 38 KB, 比直接从源文件封出来的 18800 字节大——这是重新编码的代价,不是算错了。
二、跟 RTP 那张表对比
同样写 pkt_size=1316,8.2.2 里 RTP 每包只有 1140 字节,
这里裸 UDP 却是实打实的 1316。差的正是那 12 字节的 RTP 头。
承载方式 pkt_size 每包最大 其中 RTP 头
裸 UDP 1316 1316 0 裸 UDP 1400 1400 0 RTP over UDP 1316 1140 12 RTP over UDP 1400 1328 12
算一遍就明白了:1316 减 12 是 1304,除以 188 得 6.94, 只能装 6 个 TS 包,1128 加 12 得 1140。 1400 减 12 是 1388,能装 7 个,1316 加 12 得 1328。
所以要记住一句话:pkt_size 限制的是整个包,不是负载。
加了 RTP 头,能装的 TS 包就要少一个。
三、没有序号,丢一个都不知道
这是裸 UDP 最大的问题:它连「丢了多少」都不告诉你。
承载层 有没有序号 丢了能发现吗
裸 UDP 承载 TS 没有 发现不了 RTP 承载 TS 有 能发现,但补不回来 HTTP 承载 TS TCP 序号 能发现,还能重传
TS 自己有个连续性计数器,占 4 位,能猜到中间断了几包。 但猜到了也只是知道——它没有任何重传机制。
所以「UDP 直播」这句话在工程上通常指的是 RTP over UDP,不是裸 UDP。 裸 UDP 只适合局域网、或者上层自己带纠错(比如 SRT)的场合。
四、VLC 发 UDP 的命令
用 VLC 当发送端,一条 --sout 就够:
vlc s7.mp4 --sout="#std{access=udp,mux=ts,dst=127.0.0.1:5004}"
mux=ts 指定封成 TS,access=udp 指定用 UDP 送,dst 是目的地址与端口。
收端只要 ffplay udp://127.0.0.1:5004 就能看。
这条命令有个坑:UDP 是单向的,VLC 不知道有没有人在收。
所以它会一直发,哪怕对面早就关了——这就是「推模式」的性格。
换成 access=rtp 就自动带上序号,这也是 RTP 存在的原因。
五、动手 Demo:三档 pkt_size 抓包
下面这段用一个旁路 socket 抓包,只数包数与每包大小,不解码。
同一段画面发三遍,只改 pkt_size。
import 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
for pkt in (1316, 1400, 2000):
port = free_port()
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.bind(("127.0.0.1", port))
rx.settimeout(0.5)
sizes, stop = [], [False]
def sniff():
while not stop[0]:
try:
sizes.append(len(rx.recvfrom(65535)[0]))
except Exception:
continue
th = threading.Thread(target=sniff)
th.daemon = True
th.start()
o = av.open("udp://127.0.0.1:%d?pkt_size=%d" % (port, pkt),
"w", format="mpegts")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
st.bit_rate = 400000
c, n = av.open("s7.mp4"), 0
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
n += 1
if n >= 25:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
time.sleep(0.7)
stop[0] = True
th.join(1.0)
rx.close()
print(u"pkt_size=%d 包数=%d 每包最大=%d"
% (pkt, len(sizes), max(sizes) if sizes else 0))
实跑输出:
--------------------------------------------------------------------
8.4.2 VLC 作为 UDP 流媒体服务器
--------------------------------------------------------------------
**① 换成 UDP 承载 TS,每包就是 1316 字节**
项 值
----------------------------------------
TS 总字节 18800
TS 包数 100
理论 UDP 包数(1316 一包) 15
pkt_size UDP 包数 每包最大
--------------------------------------
1316 29 1316
1400 28 1400
2000 27 2000
**跟 8.2.2 那张 RTP 的表对比一下,差别一眼就看出来:**
**同样 `pkt_size=1316`,UDP 是 1316 字节一包,RTP 只有 1140 字节。**
**因为 RTP 要扣掉自己的头,而这里裸发 UDP,`pkt_size` 就是数据报本身的大小。**
**1316 正好是 7 个 TS 包(7×188),一个字节都不浪费。**
**② 没有序号,丢一个都不知道**
层 有没有序号 丢了能发现吗
------------------------------------------------------
裸 UDP 承载 TS 没有 发现不了
RTP 承载 TS 有 能发现,但只能等
HTTP 承载 TS TCP 序号 能发现,且会重传
**所以「UDP 直播」通常不是裸 UDP,而是 RTP 骑在 UDP 上。**
**裸 UDP 只适合局域网、或者上层自己带纠错(比如 SRT)的场景。**
**③ VLC 发 UDP 的命令**
`vlc s7.mp4 --sout="#std{access=udp,mux=ts,dst=127.0.0.1:5004}"`
**`mux=ts` 指定封成 TS,`access=udp` 指定用 UDP 送,`dst` 是目的地址与端口。**
**收端只要 `ffplay udp://127.0.0.1:5004` 就能看。**
**这条命令有个坑:UDP 是单向的,VLC 不知道有没有人在收。**
**所以它会一直发,哪怕对面早就不在了——这就是「推模式」的性格。**
pkt_size 越大,包数越少,每包越接近上限——三档完全线性。
注意 ?pkt_size= 要写在 URL 里,它属于 UDP 协议层的私有选项,
不是 av.open 的通用参数,写成 options={} 反而不生效。
30 秒实验
- 把
pkt_size改成 500:看封装器还能不能装下整数个 TS 包。 - 把
pkt_size改成 100000:看会不会被内核拒绝。 - 把
udp://换成rtp://:重跑同一段,看每包是不是少了 12 字节。
常见误区
- 以为
pkt_size指的是负载长度。它限制的是整个数据报。 - 拿裸 UDP 当可靠传输用。丢包、乱序、重复,全都不管。
- 以为「包数比理论多」是代码算错了。重新编码本身就会让体积变大。
自测三问(点开对答案)
1. 为什么 1316 是 MPEG-TS over UDP 的常用值? —— 1316 等于 7 乘 188,正好装 7 个 TS 包,一个字节不浪费。 2. 同样 pkt_size=1316,为什么 RTP 每包只有 1140? —— 要扣掉 12 字节 RTP 头,1304 只够装 6 个 TS 包,6128 加 12 得 1140。 3. 裸 UDP 丢了包,接收端能知道吗? —— 只能靠 TS 的连续性计数器猜到断了几包,但补不回来。- ☐ 8.4.2 完,下一节 8.4.3 FFmpeg 实现 UDP 直播拉流