🎬 第十章 FFmpeg命令行在Linux系统中的应用
📄 一句话概括

ffprobe 只管「是什么」,ffplay 只管「看/听」,两个都不改文件。 真正动手改的永远是 ffmpeg——三件套的分工就到这里。

📝 本节和前面几节的关系

第三章讲过三大工具的分工,这一章补的是「怎么用它们排查」。 ffprobe 那十几个字段,是后面所有参数的地基;seek 的单位更是最容易踩的一处。

一、三个工具,三件事

同一个 FFmpeg 装出来有三个可执行文件,各干一件事:

工具 干什么 会不会改文件


ffmpeg 转码、转封装、加滤镜、推流 会,输出是新文件 ffprobe 读容器头,报时长、码率、流信息 不会,只读 ffplay 放出来给人看/听 不会,只读

排查的顺序永远是「先 probe、再 play、最后才 ffmpeg」: 先确认「它到底是什么」,再看「它能不能正常放」, 最后才是「我要把它变成什么」。顺序反了,就会拿着错的参数改错的东西。

二、ffprobe 把容器头念一遍

ffprobe -show_streams -show_format 会吐一大堆,其实关键的只有十几个:

字段 本片的值 它到底是什么


format_name mov,mp4,m4a,3gp,3g2,mj2 容器格式,别名一串 duration 2.00 秒 整片时长 bit_rate 941556 整片平均码率 codec_name(视频) h264 编码器名 width / height 320 / 240 画面尺寸 pix_fmt yuv420p 像素格式 r_frame_rate 25 每秒多少帧 time_base(视频) 1/12800 时间刻度,命根子 codec_name(音频) aac 音频编码器 sample_rate 44100 Hz 采样率 channels 2 声道数

format_name 那一串别名值得多看一眼: mp4 家族的容器共用同一套解析器,所以 -f 写 mp4、mov、m4a 都能开。 这也解释了 9.1 里那 414 个格式名为什么那么多——很多是同一个解析器的别名。

time_base 是这一整章最该记住的一个:它不是「帧率」,是「时间的最小刻度」。 本片是 1/12800,也就是 12800 分之 1 秒一格,一帧 25 fps 占 512 格。 所有 pts、所有 seek 的偏移,都要换算到这个刻度上才对得上。

三、seek 的单位不是秒

av.open() 出来的 seek() 收的是「按 av.time_base 计的整数」, 而 av.time_base 是百万分之一秒。 传帧号、传秒、传流自己的 time_base,三种都是错的。

要跳到 落点 pts 换成秒 偏了多少


0.5 秒 6144 0.48 秒 -20 毫秒 1.0 秒 12800 1.00 秒 +0 毫秒 1.5 秒 18944 1.48 秒 -20 毫秒

三个落点跟目标时刻的差都在一帧(40 毫秒)以内, 因为它们都落在「目标之前最近的那个可解码帧」上。 这就是 seek 的本质:不是「跳到那个时刻」,是「跳到那个时刻之前最近的落脚点」。

落脚点是什么?默认是关键帧。 本课这个测试源几乎每帧都带关键帧标记,所以它能落到「刚好前一帧」这么准; 换成正常发行的片子,一个 GOP 里只有第一帧是关键帧, seek 就只能落到那个关键帧上,要精确到某一帧必须再解码丢弃—— ffplay -ss 干的就是这件事,所以它在长片上定位会先卡一下。

四、ffplay 的八条常用参数

ffplay 的参数分两类:一类只影响「怎么看」,一类影响「看哪里」。

参数 管什么 哪里容易踩


-x / -y 窗口宽高 只影响显示,不改分辨率 -autoexit 播完就退出 不写会停在最后一帧不动 -loop 0 无限循环 后面那个 0 是「一直」 -ss 从第几秒开始 精确定位,长片上会先卡一下 -t 只播多少秒 和 -ss 配合就是截一段看 -vf 挂视频滤镜 串多条用逗号;有空格或括号才必须加引号 -nodisp 只出声不出画 纯听音频时用 -sync 音画以谁为准 默认 audio,改成 video 容易掉音

-autoexit 是新手最容易漏的一条:不加它,播完画面就僵在那儿, 看着像卡死,其实早就放完了。 批处理里循环调 ffplay 时,少这一个参数会让脚本永远停住。

五、为什么 probe 永远是第一步

因为后面每一个参数都要跟 probe 出来的值对上:

-s 要跟 width×height 对上,不然画面会拉花; -r 要跟 r_frame_rate 对上,不然会掉帧或重复帧; -ar 要跟 sample_rate 对上,不然声音会变调; -ac 要跟 channels 对上,不然会少一声道。

这四组参数写错,都不会报错,只会「结果不对」—— 画面歪、声音快、少一半声道。 而只要先 probe 一次,这些坑全都能提前绕开。

六、动手 Demo:手写一个 ffprobe,再验一次 seek 的落点

ffprobe 的输出不是黑魔法,值全在容器头里躺着。 下面这段自己造一个 2 秒的小片,再把关键字段念一遍,最后验两个 seek 落点。

from fractions import Fraction

import av

W, H, FPS, NF = 320, 240, 25, 50
SRC = "probe10.mp4"

c = av.open("testsrc=size=%dx%d:rate=%d" % (W, H, FPS), format="lavfi")
o = av.open(SRC, "w")
v = o.add_stream("libx264", rate=Fraction(FPS, 1))
v.width, v.height, v.pix_fmt = W, H, "yuv420p"
for n, f in enumerate(c.decode(c.streams[0])):
    if n >= NF:
        break
    g = f.reformat(format="yuv420p")
    g.pts, g.time_base = n, Fraction(1, FPS)
    for pk in v.encode(g):
        o.mux(pk)
for pk in v.encode(None):
    o.mux(pk)
o.close()
c.close()

cc = av.open(SRC)
print("容器 %s  时长 %.2f 秒" % (cc.format.name, float(cc.duration) / av.time_base))
for s in cc.streams:
    k = s.codec_context
    print("%-5s %-6s %dx%d %-9s 时间基 %s" %
          (s.type, k.name, k.width, k.height, k.pix_fmt, s.time_base))
cc.close()

for t in (0.5, 1.0):
    cc = av.open(SRC)
    cc.seek(int(t * av.time_base))
    for f in cc.decode(cc.streams.video[0]):
        print("seek %.1f 秒 -> 首帧 pts %d" % (t, f.pts))
        break
    cc.close()

实跑输出:

--------------------------------------------------------------------
10.2 使用 ffplay 和 ffprobe
--------------------------------------------------------------------

**① ffprobe 干的活:把容器的头拆开念一遍**

  ffprobe 字段        本片的值                  它到底是什么
  ------------------------------------------------------------------------
  format_name         mov,mp4,m4a,3gp,3g2,mj2   容器格式,别名一串
  duration            2.00 秒                   整片时长
  bit_rate            941556                    整片平均码率
  size                235389 字节               文件多大
  codec_name(视频)  h264                      编码器名
  width / height     320 / 240                画面尺寸
  pix_fmt             yuv420p                   像素格式
  r_frame_rate        25                        每秒多少帧
  time_base(视频)   1/12800                   时间刻度,命根子
  codec_name(音频)  aac                       音频编码器
  sample_rate         44100 Hz                  采样率
  channels            2                         声道数

**这十三行就是 `ffprobe` 输出的骨架。`ffprobe -show_streams` 多出来的那些,**
**大部分是这几项的衍生(`avg_frame_rate` 由总帧数除时长算出来)。**

**② 最容易踩的一处:`seek` 的单位不是秒**

**`av.open()` 出来的 `seek()` 收的是「按 `av.time_base` 计的整数」,**
**`av.time_base` 是百万分之一秒。传帧号、传秒、传流自己的 `time_base` 都错。**

  要跳到      落点 pts        换成秒        偏了多少
  ----------------------------------------------------------
  0.5 秒      6144            0.48 秒       -20 毫秒
  1.0 秒      12800           1.00 秒       +0 毫秒
  1.5 秒      18944           1.48 秒       -20 毫秒

**三个落点跟目标时刻的差都在一帧(40 毫秒)以内——**
**这不是精度不够,是「只能落到关键帧上」。**

**本片一共 50 个包,其中 50 个都带关键帧标记**——几乎每帧都是。
**所以上面的 seek 才能落到「刚好前一帧」这么准。**

**这是本课测试源的特点:帧是一张张图直接喂给编码器的。**
**换成正常发行的片子,一个 GOP 里只有第一帧带这个标记,**
**seek 就只能落到那个关键帧上,要精确到某一帧必须再解码丢弃——**
**`ffplay -ss` 干的就是这件事,所以它在长片上定位会先卡一下。**

**③ ffplay 的参数,记住这几条就够用**

  参数          管什么                        哪里容易踩
  --------------------------------------------------------------------------
  -x / -y      窗口宽高                      只影响显示,不改分辨率
  -autoexit     播完就退出                    不写会停在最后一帧不动
  -loop 0       无限循环                      后面那个 0 是「一直」
  -ss           从第几秒开始                  精确定位,长片上会先卡一下
  -t            只播多少秒                    和 -ss 配合就是截一段看
  -vf           挂视频滤镜                    串多条用逗号,要加引号
  -nodisp       只出声不出画                  纯听音频时用
  -sync         音画以谁为准                  默认 audio,改 video 会掉音

**`ffprobe` 只管「是什么」,`ffplay` 只管「看/听」,两个都不改文件。**
**真正动手改的永远是 `ffmpeg`——这三件套的分工就到这里。**

读出来的那行 1/12800 就是 time_base:一帧 512 格,25 帧正好 12800 格一秒。 seek 那句 int(t * av.time_base) 是唯一的正确写法—— 把秒换算成「百万分之一秒」,塞给 seek()。

代码块里这段源只有视频流,所以只打出一行; 上面那张表里的音频字段,是带立体声的那份源跑出来的。 换成你自己的片子,for s in cc.streams 会自动把音频流也列出来。

30 秒实验

  1. 把 int(t * av.time_base) 改成 int(t / vs.time_base) 再跑:看落点跑到哪去了。
  2. 把 any_frame=True 加上:对比默认行为,理解「只落关键帧」和「任意帧」的差别。
  3. 用真片子跑一次 ffprobe -show_streams:数一数有多少字段是上面这张表没提到的。

常见误区

  1. 把 seek() 的参数当秒传。它要的是微秒整数,传 1 就是 1 微秒。
  2. 以为 seek 能精确到某一帧。默认只落关键帧,精确帧要解码丢弃。
  3. ffplay 忘了 -autoexit。播完画面僵住,看着像卡死。
自测三问(点开对答案) 1. `time_base` 是 1/12800 时,25 fps 的一帧占几格? —— 512 格。12800 除 25 就是 512。 2. `seek()` 的参数单位是什么? —— 百万分之一秒(`av.time_base`)。传秒或传帧号都会静默落到别处。 3. 为什么 seek 落到「目标之前」而不是「目标之后」? —— 因为解码必须从关键帧开始。落点之后只能往后续,不能往前补。

相关笔记