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

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

  1. 把丢包率调到每 20 包丢 1:帧数掉得更少,说明丢得少能靠缓冲补回来。
  2. 先丢第一个包:关键帧丢了,后面的帧基本全废。
  3. 给 mpeg4 也配一份 SDP:看帧数会不会比裸 RTP 更多(会,探测更准)。

常见误区

  1. 以为 H.264 解不出是传输问题。是缺 SPS/PPS。
  2. 丢包后以为程序会报错。UDP 不报错,只是帧数变少。
  3. 拿包数当帧数。一帧可能好几个包,也可能几帧一个包。
自测三问(点开对答案) 1. 裸 RTP 里 H.264 为什么解不出? —— 缺少 SDP 里的 sprop-parameter-sets,解码器拿不到 SPS 与 PPS。 2. 丢一半包,帧数会掉多少? —— 实测大约掉一半(22 掉到 11),大致成比例。 3. 为什么要加 -protocol_whitelist? —— ffmpeg 默认不允许随便连外部协议,不加会报协议不在白名单。

相关笔记