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

一条 ffmpeg 命令分三段:全局、输入、输出。 同名参数放在不同段,含义可能完全相反——-ss 就是最典型的例子。

📝 本节和前面几节的关系

第一章讲的是零件:七个库、八个工具、五阶段流水线。 从这一节起开始下命令,本章十一条命令都是日常最常用的那批。

一、一条命令的骨架

先把骨架背下来,后面所有命令都是往这个框里填东西:

位置 写什么 以什么为界


全局段 -y、-loglevel、-hide_banner 到第一个 -i 之前 输入段 -i 输入文件 前面的那些 -i 与输入文件名 输出段 输出文件名 前面的那些 最后一个输出文件名

段的边界不是靠空格分的,是靠 -i 和输出文件名这两个「地标」分的。 所以同一条命令里,-ss 可以出现两次,各管一段,互不干扰。

这也是新手最常踩的坑:以为参数写哪儿都一样,只是「顺序不同」。 实际上 ffmpeg 是边解析边执行的,读到哪个地标就把前面的参数归给那一段。

二、同名不同义:-ss 的两个位置

-ss 是最有名的例子,它的两种写法做的事根本不一样:

写法 做法 代价


-ss 4 -i 输入 直接跳到第 4 秒附近 快,但只能落在关键帧上 -i 输入 -ss 4 从头解到第 4 秒再丢 慢,但帧精确

第一种之所以快,是因为它连解码都省了:容器里有关键帧的位置表, 直接跳到最近的入口,从那里往后解几帧。

第二种之所以准,是因为它真的把前面每一帧都解开了,只是不输出。 所以「哪一种更好」没有统一答案,要看你更在意速度还是更在意精确。

具体差多少,下面 Demo 用同一份素材量了一遍。 结论是读的帧数差好几倍——而这个倍数会随素材变长、目标位置变远继续放大。

三、先记住这三条

不追求一次记全,先把这三条用熟,已经能覆盖大半日常:

参数 作用 为什么常用


-y 覆盖输出文件不问 批处理时不能有人守着回车 -loglevel 控制日志多少 排错时开 verbose,正常时开 error -hide_banner 不打印版本信息 日志要进程序解析时先去掉横幅

第一行有个隐蔽的坑:不加 -y 时,如果输出文件已存在, ffmpeg 会停下来等你敲 y。在脚本里没人敲,它就那么在后台挂着不动。 表现是「脚本跑着跑着没反应了」,实际是卡在这个确认上。

第二行值得多说一句:-loglevel 有七档,从 quiet 到 debug。 调不出来的时候先开 info,还不行开 verbose——ffmpeg 的日志里通常直接写了原因。

四、动手 Demo:参数位置决定含义

这一段不调 ffmpeg.exe(沙箱里没有),而是用 PyAV 复现 -ss 两种写法的行为, 拿同一份 5 秒素材、同一个目标时刻,分别量一遍「读了多少帧」。

import time
import av
import numpy as np

# 造 125 帧(5 秒)的素材
c = av.open(u"src.mp4", u"w")
s = c.add_stream(u"libx264", rate=25)
s.width, s.height, s.pix_fmt = 160, 90, u"yuv420p"
for i in range(125):
    a = np.zeros((90, 160, 3), dtype=np.uint8)
    a[:, :, 0] = (i * 2) % 256
    f = av.VideoFrame.from_ndarray(a, format=u"rgb24")
    for pk in s.encode(f):
        c.mux(pk)
for pk in s.encode(None):
    c.mux(pk)
c.close()

TARGET = 4.0                       # 想要第 4 秒那一帧

# 做法一:先跳(-ss 放在 -i 前面)
t0 = time.perf_counter()
c = av.open(u"src.mp4")
c.seek(int(TARGET * 1000000))
n1 = 0
for f in c.decode(video=0):
    n1 += 1
    if f.time is not None and f.time >= TARGET:
        break
t1 = (time.perf_counter() - t0) * 1000
c.close()

# 做法二:从头解(-ss 放在 -i 后面)
t0 = time.perf_counter()
c = av.open(u"src.mp4")
n2 = 0
for f in c.decode(video=0):
    n2 += 1
    if f.time is not None and f.time >= TARGET:
        break
t2 = (time.perf_counter() - t0) * 1000
c.close()

print(u"先跳 %.2f 毫秒 / %d 帧" % (t1, n1))
print(u"从头 %.2f 毫秒 / %d 帧" % (t2, n2))

实跑输出:

--------------------------------------------------------------------
FFmpeg 命令行简介——参数位置决定含义
--------------------------------------------------------------------

**① 一条命令分的不是「选项」和「文件」,是「哪一段」**

  段           管什么                         写错了会怎样
  ------------------------------------------------------------------------
  全局参数     影响整个程序,比如日志级别     多半不报错,只是没反应
  输入参数     只管紧跟的那个 -i              被当成输出参数,行为反了
  输出参数     只管后面那个输出文件           被当成输入参数,报错

**所以同名的参数出现在不同位置,意思可能完全不同。最典型的就是 -ss。**

**② 同一个 -ss,两个位置,两种做法**

  做法                耗时(毫秒)     读了几帧     对应命令行
  ------------------------------------------------------------------------
  先跳到关键帧        3.25             15           -ss 4 放在 -i 前面
  从头解到第 4 秒     5.96             101          -ss 4 放在 -i 后面

**为了拿到同一时刻的同一帧,一种读了 15 帧,另一种读了 101 帧。**
**差距是「跳过了多少帧」——素材越长、目标越靠后,这个差距越大。**
**先跳那种为了拿到第 4.0 秒,实际读了 15 帧;落点在 4.00 秒处,**
**因为 seek 只能落在关键帧上,从关键帧往后再解几帧才够到目标。**
**这一次两者最终停在同一个 pts(51200),说明落点正好对上了。**
**但这是运气:关键帧稀的片子上,落点会比你要的早半秒甚至几秒。**
**耗时这次只差 1.8 倍,因为素材太短(5 秒)、分辨率太小(160×90)。**
**换成 4K 一小时的片子,解码耗时变成几十秒,这个倍数才会显出真实量级。**

**③ 这条规则适用于所有「同名不同义」的参数**

  参数     放在输入前               放在输出前
  ------------------------------------------------------------
  -ss      先跳,快,可能不精确     解到位置再丢,慢,精确
  -t       只读这么多进来           只写这么多出去
  -r       按这个帧率解读输入       按这个帧率生成输出

**记不住也没关系,有个笨办法很管用:不确定就放到输出前。**
**输出前的语义更符合直觉——「我要的结果是这样」,而不是「你按这个理解输入」。**

两条路都加了同一句 break,所以比的才是「为了拿到那一帧花了多少功夫」。 不加这一句,两条路都会把全片解完,差距就被抹平了。

读帧数那一列才是真结论:一个读了个位数到十几帧,另一个读了上百帧。 这个差距跟素材长短成正比——放到一小时的片子上,就是从「几帧」变成「几万帧」。

耗时那一列的倍数比读帧数的倍数小得多,原因有两个:素材只有 5 秒, 分辨率只有 160×90,解码本身太便宜。开销里固定成本(打开文件、初始化解码器)占了大头。

所以看这类对比不要只看毫秒:毫秒受机器状态影响很大, 「读了几帧」是结构性差异,换台机器也一样。

30 秒实验

  1. 换位置:同一条 -ss 4,分别放在 -i 前和 -i 后,用 -vframes 1 只出一帧,比较耗时。
  2. 看日志:加 -loglevel verbose 跑一次,你会看到它自己报出「seek 到哪个位置」,比猜有用。
  3. 踩一次坑:故意不加 -y 覆盖一个已存在的文件,看它在哪里卡住。以后遇到「脚本没反应」就有数了。

常见误区

  1. 以为参数写在哪儿都一样。-ss 放前面是跳,放后面是解到位置再丢,快慢差好几倍。位置是语义的一部分,不是格式问题。
  2. 脚本里不加 -y。输出文件已存在时它会等人确认,脚本里没人敲键盘就卡死。批处理一律加 -y。
  3. 把 -loglevel 当装饰。它控制的是日志详略,调不出来时开到 verbose,ffmpeg 的报错通常已经把原因写清楚了。
自测三问(点开对答案) 1. 为什么 `-ss` 放在 `-i` 前面会快? —— 因为容器里有关键帧位置表,它直接跳到目标附近的关键帧,从那里往后解几帧。跳过了前面所有的解码工作。 2. 放在后面那种写法,既然解开了前面的帧,为什么不干脆用上? —— 它确实解开了,但只把时间戳达到目标之后的帧送进输出,前面的全部丢弃。所以准,但白做了一遍工。 3. 一条命令里有两次 `-ss`,会不会冲突? —— 不会。第一个归输入段(只管那个 `-i`),第二个归输出段。两段各管各的,这正是「按地标分段」的意思。

相关笔记