一条流一份档案,八项里最该看的是 time_base 和 pix_fmt。 nb_frames 常常没有或者不准,别拿它当准数。
3.3.3 钻到最细的帧,这一节回到中间层——每条流的属性。 3.3.5 讲输出格式,3.3.6 讲怎么在多流里挑一路。
一、八项里最该看什么
一条流的属性很多,但真正影响后续操作的只有八项:
字段 含义 为什么重要
index 流的编号 -map 要靠它选 codec_name 编码名 决定能不能解 width / height 分辨率 决定解码压力 pix_fmt 像素格式 决定能不能挂滤镜 time_base 时间戳的单位 算时长必须用它 avg_frame_rate 平均帧率 分母为 0 说明有问题 nb_frames 容器里记的帧数 常常没有或不准 bit_rate 这条流的码率 估带宽
第五行最容易踩坑:time_base 是「一个时间戳单位等于多少秒」,不是帧率。
1/12800 的意思是「一个单位 = 1/12800 秒」,所以 pts 涨 512 就是 40 毫秒。
算时长必须用它:pts × time_base 才是秒数。
直接拿 pts 当毫秒用,是最常见的「算出来时长差了十几倍」的原因。
二、nb_frames 为什么不可靠
这个字段的正确用法是「参考」,不是「相信」:
容器 nb_frames 有值吗 原因
mp4 常有,但不一定对 记在索引里,录制中断就偏 mkv 常常是 N/A 不预先记录总帧数 裸流 h264 一定没有 根本没有容器这一层
想要准确帧数只有一个办法:解一遍数。
这就是 -count_frames 存在的理由——它会让 ffprobe 真的去数,代价是慢。
对 mkv 尤其要注意:它边写边记,结尾才能知道总数, 所以文件正在写的时候读它,nb_frames 一定是 N/A。
三、音频流没有宽高
同一个文件里,视频流有 width/height,音频流没有——这不是读错了:
流类型 有宽高吗 有哪些独有字段
视频 有 pix_fmt、avg_frame_rate 音频 没有 sample_rate、channels、sample_fmt 字幕 没有 几乎没有独有字段
所以写脚本解析时,必须先看类型,再决定取哪些字段。
一律去取 width 的结果是——在音频流上直接抛异常,整个脚本中断。
更稳的写法是「先按类型分组,再分别处理」。
这也是 3.3.6 那个 -select_streams 的价值:它让你一次只面对一种类型。
四、动手 Demo:一条流一份档案
用一个「视频+音频」的双流文件,把两条流的档案都打出来。 重点是看:音频那两列确实是空的,以及时间基两条流不一样。
import av
c = av.open(u"av3.mp4")
for s in c.streams:
cc = s.codec_context
w = getattr(cc, u"width", 0)
h = getattr(cc, u"height", 0)
print(u"#0:%d %-6s %-8s %sx%s 时间基 1/%d"
% (s.index, s.type, cc.name, w or u"-", h or u"-",
s.time_base.denominator))
# 音频的独有字段
for s in c.streams.audio:
cc = s.codec_context
print(u"音频:采样率 %d 声道 %d" % (cc.sample_rate, cc.channels))
c.close()
实跑输出:
--------------------------------------------------------------------
ffprobe -show_streams——一条流一份档案
--------------------------------------------------------------------
**① 流段里最该看的八项**
字段 含义 为什么重要
---------------------------------------------------------------
index 流的编号 -map 要靠它选
codec_name 编码名 决定能不能解
width / height 分辨率 决定解码压力
pix_fmt 像素格式 决定能不能挂滤镜
time_base 时间戳的单位 算时长必须用它
avg_frame_rate 平均帧率 分母为 0 就说明有问题
nb_frames 容器里记的帧数 常常没有或不准
bit_rate 这条流的码率 估带宽
**第五行最容易踩坑:time_base 是「一个时间戳单位等于多少秒」,不是帧率。**
**1/12800 的意思是「一个单位 = 1/12800 秒」,所以 pts 涨 512 就是 40 毫秒。**
**② 本机这个双流文件的流段**
编号 类型 编码 宽 高 时间基
------------------------------------------------------
0 video h264 160 90 1/12800
1 audio aac — — 1/44100
**注意音频那两列是空的——音频没有宽高,这是正常的,不是读错了。**
**所以脚本里解析时要先看类型,别一律去取 width。**
**③ nb_frames 这个字段为什么不可靠**
容器 nb_frames 有值吗 原因
---------------------------------------------------------------------
mp4 常有,但不一定对 记在索引里,录制中断就偏
mkv 常常是 N/A 不预先记录总帧数
裸流(h264/es) 一定没有 根本没有容器这一层
**想要准确帧数只有一个办法:解一遍数。**
**这就是 `-count_frames` 这个参数存在的理由——它会让 ffprobe 真的去数。**
**④ streams 和 packets 的编号是对得上的**
**流段里 index=1 是音频,包段里 `-select_streams a` 拿到的就是它。**
**两处的编号是同一套,所以 `-map 0:1` 和 `-select_streams a` 指向同一个东西。**
getattr(cc, "width", 0) 那个兜底不是多余的:
音频的 codec context 上确实没有 width 属性,直接取会抛 AttributeError。
注意两条流的时间基可能不一样。 视频常见 1/12800,音频常见 1/44100 或 1/1000000——各按各的采样节奏记。 所以混算时长时必须分别换算,不能用同一个时间基。
s.time_base.denominator 拿到的是分母。
完整写出来是「分子/分母」,常见的时间基分子都是 1,但不敢假设——
有的封装会用 1001/30000 这种形式,倒过来读就错了。
30 秒实验
- 看时间基:
ffprobe -show_streams 片子.mp4 | grep time_base,两条流各是多少。 - 查帧率:
ffprobe -show_streams 片子.mp4 | grep frame_rate,两个值可能不同。 - 数准帧数:
ffprobe -count_frames -show_streams 片子.mp4 | grep nb_read_frames。
常见误区
- 拿 pts 当毫秒用。pts 是时间基的倍数,不是毫秒。要乘 time_base 才是秒。
- 相信 nb_frames。它只是声明值,mkv 里常常是 N/A。要准就
-count_frames。 - 对音频流取 width。音频没有宽高,直接取会抛异常。先判断类型再取字段。
自测三问(点开对答案)
1. time_base 是 1/12800,pts 是 512,这一帧在第几秒? —— 512 × (1/12800) = 0.04 秒,也就是第 40 毫秒。 2. 为什么音频流没有 width 和 height? —— 因为它不是画面。音频的对应属性是采样率、声道数、采样格式,完全另一套。 3. r_frame_rate 和 avg_frame_rate 有什么区别? —— 前者是「标称帧率」(按时间基算出来的基准),后者是「实际平均帧率」。可变帧率素材上两者会不同。- ☐ (本节完,下一节 3.3.5 print_format)