🎬 第二章 FFmpeg命令行初体验
📄 一句话概括

ffprobe 是「不问不知道」的工具:四层信息,四个开关。 它的价值不只是给人看,更在于 -print_format json 让它变成程序接口。

📝 本节和前面几节的关系

2.1 到 2.5 都在「产出」,这一节换个方向——先看清手上是什么。 排障时它通常是第一条该敲的命令。

一、四种问法,四层信息

ffprobe 的参数不多,常用的就四个,各看一层:

参数 看哪一层 信息量


-show_format 整个文件 时长、码率、容器名 -show_streams 每条流 编码、分辨率、时间基 -show_packets 每一个包 几百上千行 -show_frames 每一帧 要全解一遍,最慢

从上到下越来越细,也越来越慢。 -show_format 几乎瞬间返回;-show_frames 要把整片解一遍。

所以排障的顺序也应该从上往下:先看 format 有没有时长, 再看 streams 的编码对不对,最后才去看 packet 和 frame。

二、这四层各自能回答什么问题

举个具体的例子,同样一句「这个文件放不了」,四层给出四种答案:

层 能看出什么 典型问题


format 容器认不认、时长有没有 时长是 N/A,说明索引坏了 stream 编码是不是本机支持的 编码名很冷门,播放器不认 packet 时间戳从几开始、有没有跳变 pts 从 10000 起,播放就卡 frame 解码出来的画面正不正常 帧数是 0,说明解码失败

第三行是很经典的一类问题:pts 不从 0 开始。 很多播放器假设第一帧的 pts 是 0,结果拿到 10000 就开始算错,表现是开头卡住几秒。

第四行的「帧数 0」则说明问题在解码器上,不在容器上—— 这两层的区别很重要,决定了你去换播放器还是换文件。

三、加一个开关,工具变接口

-print_format json 是 ffprobe 最被低估的一个参数。 加了它,输出就从「给人看的表格」变成「给程序解析的结构」。

输出格式 适合谁 为什么


默认文本 人眼扫一眼 字段对齐,看着舒服 json 程序 结构明确,不怕字段顺序变 flat shell 脚本 一行一个键值,grep 好切

做自动化流水线时,解析 JSON 比用正则切字符串可靠一个数量级。 因为 JSON 的字段顺序变了不影响解析,而切字符串的方案一改版就崩。

这个区别在长期维护里特别明显: 用 JSON 的脚本,ffmpeg 升级几版都不用动;用文本切的脚本,每次升级都可能要改。

四、动手 Demo:四个视图

拿同一份素材,用四个层次分别探一遍,看每一层能拿到什么。 这也是排障时的固定动作:从粗到细,一层一层往下确认。

import av

c = av.open(u"src.mp4")

# 第一层:整个文件(相当于 -show_format)
print(u"容器 %s" % c.format.name)
print(u"时长 %.0f 微秒" % float(c.duration))
print(u"流数 %d" % len(c.streams))

# 第二层:每条流(相当于 -show_streams)
st = c.streams.video[0]
print(u"编码 %s %dx%d" % (st.codec_context.name, st.width, st.height))
print(u"时间基 1/%d" % st.time_base.denominator)

# 第三层:每个包(相当于 -show_packets)
npk = 0
total = 0
biggest = 0
for pk in c.demux(video=0):
    if not pk.size:
        continue
    npk += 1
    total += pk.size
    biggest = max(biggest, pk.size)
print(u"包 %d 个,合计 %d 字节,最大 %d" % (npk, total, biggest))
c.close()

# 第四层:每一帧(相当于 -show_frames,最慢)
c = av.open(u"src.mp4")
nf = sum(1 for _ in c.decode(video=0))
c.close()
print(u"解码出 %d 帧" % nf)

实跑输出:

--------------------------------------------------------------------
ffprobe 获取视频信息——四个视图
--------------------------------------------------------------------

**① ffprobe 的四种问法,对应四层信息**

  参数              看哪一层     信息量
  -------------------------------------------------------
  -show_format      整个文件     时长、码率、容器名
  -show_streams     每条流       编码、分辨率、时间基
  -show_packets     每一个包     几百上千行
  -show_frames      每一帧       几百上千行

**② 本机这份素材的四层信息**

  层         看到了什么
  ----------------------------------------------------------------------
  format     容器 mov,mp4,m4a,3gp,3g2,mj2,时长 5000000 微秒,1 条流
  stream     h264 160×90,时间基 1/12800
  packet     125 个包,合计 7696 字节,最大一个 731 字节
  frame      解码后才看得到,125 帧

**③ 拿这一步做排障的固定动作**

**不确定参数怎么写的时候,先跑 -show_streams 看时间基和像素格式,**
**再跑 -show_packets 看时间戳是不是从 0 开始。**
**这两条能把「输出文件时间戳错乱」这类问题定位掉大半。**

**④ 加上 -print_format json,输出就能直接给程序用**

**这是 ffprobe 最被低估的一项:它让 ffprobe 从「给人看的工具」变成「给程序用的接口」。**
**做自动化流水线时,解析 JSON 比切字符串可靠一个数量级。**

四段代码对应四个开关,一层比一层细,也一层比一层慢。 最后那一行 sum(1 for _ in decode()) 要把整片解一遍,所以它最贵。

这一次包数和帧数正好都是 125,看着一样大。但两者不是同一个概念。 demux 切出来的是容器的段,里面还带零长度的结束标记;decode 出的是真实画面。 含 B 帧的片子上,这两个数就会对不上,包数通常会多一点。

第三层那个「最大一个多少字节」很实用: 如果最大包大得离谱(比如几十 MB),基本可以确定有一帧是关键帧且塞满了细节, 这种片子在弱设备上播放容易在那一帧卡一下。

还有一点:如果第四层解出的帧数是 0,说明解码器根本没出帧—— 这跟「容器坏了」是两个完全不同的问题,处理方向也不一样。

30 秒实验

  1. 看一遍:ffprobe -show_streams 输入.mp4,重点看 time_base、pix_fmt、bit_rate 三项。
  2. 给程序用:ffprobe -v error -print_format json -show_format -show_streams 输入.mp4,把结果丢给 Python 解析。
  3. 查时间戳:ffprobe -show_packets -select_streams v 输入.mp4 | head -20,看第一个包的 pts 是不是 0。

常见误区

  1. 排障先看 frame 层。它最慢,而且大部分问题在 format 和 stream 两层就看清了。从粗往细查。
  2. 用正则切 ffprobe 的文本输出。字段顺序和缩进都可能变,一升级脚本就崩。要程序读就用 JSON。
  3. 把包数当成帧数。包数是容器里切出来的段数,含零长度包;帧数要解码后才知道,通常更少。
自测三问(点开对答案) 1. 为什么排障要从 `-show_format` 开始,而不是直接 `-show_frames`? —— 因为它最快,而且很多问题在这一层就露出来了(比如时长是 N/A 说明索引坏了)。从粗到细查,省时间。 2. pts 不从 0 开始会有什么表现? —— 不少播放器假设第一帧 pts 是 0,拿到大数值就会算错进度,表现为开头卡住几秒或者进度条乱跳。 3. `-print_format json` 解决了什么实际问题? —— 让脚本能稳定解析。文本输出的字段顺序和缩进会随版本变,JSON 的结构不随顺序变,升级不用改脚本。

相关笔记