🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

裸 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 秒实验

  1. 把 pkt_size 改成 500:看封装器还能不能装下整数个 TS 包。
  2. 把 pkt_size 改成 100000:看会不会被内核拒绝。
  3. 把 udp:// 换成 rtp://:重跑同一段,看每包是不是少了 12 字节。

常见误区

  1. 以为 pkt_size 指的是负载长度。它限制的是整个数据报。
  2. 拿裸 UDP 当可靠传输用。丢包、乱序、重复,全都不管。
  3. 以为「包数比理论多」是代码算错了。重新编码本身就会让体积变大。
自测三问(点开对答案) 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 的连续性计数器猜到断了几包,但补不回来。

相关笔记