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 秒实验
- 把
int(t * av.time_base)改成int(t / vs.time_base)再跑:看落点跑到哪去了。 - 把
any_frame=True加上:对比默认行为,理解「只落关键帧」和「任意帧」的差别。 - 用真片子跑一次
ffprobe -show_streams:数一数有多少字段是上面这张表没提到的。
常见误区
- 把
seek()的参数当秒传。它要的是微秒整数,传 1 就是 1 微秒。 - 以为
seek能精确到某一帧。默认只落关键帧,精确帧要解码丢弃。 ffplay忘了-autoexit。播完画面僵住,看着像卡死。
自测三问(点开对答案)
1. `time_base` 是 1/12800 时,25 fps 的一帧占几格? —— 512 格。12800 除 25 就是 512。 2. `seek()` 的参数单位是什么? —— 百万分之一秒(`av.time_base`)。传秒或传帧号都会静默落到别处。 3. 为什么 seek 落到「目标之前」而不是「目标之后」? —— 因为解码必须从关键帧开始。落点之后只能往后续,不能往前补。- ☐ 10.2 完——下一节 10.3 使用 FFmpeg 实现文字水印及跑马灯