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

拉流=四问答对 + 有人把 RTP 送出来,就这么两件事。 实测 10 帧、28 个 RTP 包、首帧 999 毫秒。

📝 本节和前面几节的关系

8.1.2 讲服务器要回什么,这一节把两端接起来真跑一遍。 跑通之后,「RTSP 拉流」这件事就从名词变成了一个能手写的流程。

一、拉流到底在拉什么

先把「拉」这个字的含义说清楚:数据是服务器发的,但开关在客户端手上。 客户端不说 PLAY,服务器就一直等着。

阶段 做什么 字节量级


建立连接 TCP 连到 554(或 8554) 几十字节 协商 OPTIONS/DESCRIBE/SETUP 三百多字节 开播 PLAY,然后 RTP 开始流 每秒几十 KB

协商那几百字节,跟后面的数据量完全不在一个量级。 但它一旦出错,后面一个字节都不会有——这就是握手的分量。

所以「拉流失败」要分两步查:先看协商对不对,再看 RTP 有没有来。 两种失败的现像完全不同:协商失败是立刻报错,RTP 不来是「连上了但没画面」。

二、H.264 怎么装进 RTP

RTSP 协商完之后,真正干活的是 RTP。H.264 要装进去,得先拆。 mp4 里存的是 AVCC 格式:每段前面四个字节写长度,后面跟一段 NAL。

实测素材首包有两个 NAL,长度 672 与 2725 字节。 前者是 SPS/PPS 加一小段,后者是一整帧的主体。

情况 怎么装 代价


NAL ≤ 1400 字节 一个 RTP 包装一段 最省 NAL > 1400 字节 FU-A 分片,首尾带标志 多几个包

1400 不是随便挑的:加上 12 字节 RTP 头与 28 字节 IP/UDP 头, 刚好 1440,给隧道封装留了余量,不会触发 IP 分片。

FU-A 的规则:每片第一个字节改写成「分片标志」, 第二个字节里放「是不是第一片、是不是最后一片」。 接收端按序号拼回去,少一片这一帧就废了。

三、实测:10 帧、28 个 RTP 包

把素材拆成 NAL、按 RTP 打出去,让 libav 当客户端来拉:

项 值


总包数(50 帧) 50 最大单 NAL 2725 字节 解出帧数 10 首帧耗时 999 毫秒 服务器发出 RTP 包 28

10 帧用了 28 个 RTP 包,平均一帧接近三个包—— 因为一帧里通常有多个 NAL(参数集、切片头、数据),每个都要单独打。

首帧 999 毫秒这件事要解释清楚:不是网络慢。 libav 打开输入时会先「探测」:读一段数据判断格式、找关键帧、 等解码器初始化完才吐第一帧。 本地回环的传输时间只有零点几毫秒,剩下的九百多毫秒全在探测与缓冲上。

所以直播的「首帧延迟」里,协议与网络往往不是大头, 大头是播放端的缓冲策略——想更快的播放器会把探测窗口压小,代价是更容易判错格式。

四、命令行的两个开关

真用 ffmpeg 拉流,两个开关必须知道:

ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1:8554/live -c copy out.mp4

开关 作用 不加会怎样


-rtsp_transport tcp SETUP 写 TCP 默认 UDP,被拦就没画面 -c copy 不重编码,只搬包 白解一遍再压一遍

两个开关管的是完全不同的两件事:一个管「怎么连」,一个管「要不要重算」。 很多人把它们混着记,结果该快的地方慢、该稳的地方不稳。

还要注意录制直播流的时长控制:不加 -t 它会一直录到流断。 录直播建议加 -t 60 或者用 -f segment 按时间切片,否则磁盘很快就满了。

五、动手 Demo:十帧走完一条 RTSP

下面这段代码先把素材拆成 NAL,再起一个只做协商的服务器, 然后让 libav 自己连上来拉流,量帧数、首帧耗时与发出包数。

import av, socket, struct, threading, time

def nals_of(path, limit=50):
    c = av.open(path)
    vs = c.streams.video[0]
    out = []
    for p in c.demux(vs):
        if p.size == 0:
            continue
        b = bytes(p)
        ns = []
        off = 0
        while off + 4 <= len(b):
            L = struct.unpack(">I", b[off:off + 4])[0]
            off += 4
            if L:
                ns.append(b[off:off + L])
                off += L
        out.append(ns)
        if len(out) >= limit:
            break
    c.close()
    return out

pk = nals_of(u"s7.mp4")
print(u"包数 %d  首包 NAL %s" % (len(pk), [len(x) for x in pk[0]]))
print(u"最大单 NAL %d 字节" % max(len(n) for ns in pk for n in ns))

实跑输出:

--------------------------------------------------------------------
8.1.3 FFmpeg 实现 RTSP 直播拉流
--------------------------------------------------------------------

**① 素材先拆成 NAL,再按 RTP 打出来**

**mp4 里存的是 AVCC 格式:每段前面四个字节写长度。**
**实测首包 2 个 NAL,长度 672/2725。**

  项               值
  ---------------------------------------------
  总包数           50
  最大单 NAL       2725 字节
  RTP 负载上限     1400 字节
  超过就要分片     FU-A,首尾各带一个标志位

**② 起服务器,让 libav 自己来拉**

  项                    值
  ----------------------------------
  解出帧数              10
  首帧耗时              1004 毫秒
  服务器发出 RTP 包     28
  客户端结果            成功

**这一步是全章最关键的一条结论:RTSP 拉流「跑通」不需要真的 VLC,**
**只要有人把 OPTIONS/DESCRIBE/SETUP/PLAY 这四问答对,再把 RTP 送出去就行。**

**③ 命令行长这样**

`ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1:8554/live -c copy out.mp4`

**`-rtsp_transport tcp` 就是让客户端在 SETUP 里写 `RTP/AVP/TCP`。**
**不加这句默认走 UDP,防火墙一拦就没画面——这是 RTSP 拉流最常见的坑。**

**④ 拉流的三个「看不见的成本」**

**第一是握手四问四答,每次连接都要走一遍;**
**第二是 SDP 与 SPS/PPS 的解析,参数不对后面全黑;**
**第三是时间戳换算——RTP 用 90 kHz 时钟,mp4 用 25 分之一秒。**

**这三笔成本都在「第一个像素出现之前」付掉了,**
**所以拉流的首帧延迟天生比播本地文件大。**

首包两个 NAL、长度 672 与 2725,跟节里那张表一致。

2725 超过 1400 的上限,所以它必须分片。 这就是「一帧往往要好几个 RTP 包」的由来——不是帧大,是单包有上限。

代码里只读了 NAL 的长度,没看内容。 第一段通常带 SPS/PPS,后面才是切片数据;想验证可以打第一段的头两个字节。

30 秒实验

  1. 改上限:把 1400 改成 600,看 RTP 包数涨多少。
  2. 只取 5 帧:把握手做完就断,观察服务器端 ConnectionAborted 什么时候出现。
  3. 换成 udp 传输:跑一遍 UDP 模式,比较首帧耗时是否更短。

常见误区

  1. 以为首帧慢是网络慢。本地回环也是九百多毫秒,慢在探测与缓冲。
  2. 不加 -rtsp_transport tcp。防火墙一拦,连上了但没画面。
  3. 录直播不加时长。会一直录到流断,磁盘容易被写满。
自测三问(点开对答案) 1. RTSP 拉流的第一帧为什么慢? —— libav 要先探测格式、找关键帧、初始化解码器,本地回环也要近一秒。 2. NAL 超过 1400 字节怎么办? —— 用 FU-A 分片,首片与末片各带一个标志位,接收端按序号拼回。 3. -c copy 省掉了什么? —— 省掉解码与重编码,只搬包,几乎不花 CPU。

相关笔记