🎬 第三章 FFmpeg三大常用工具及应用选项详解
📄 一句话概括

frames 是最细的一层,也是唯一能证明「解码成功」的一层。 它慢的原因只有一个:真的把每一帧解出来了。

📝 本节和前面几节的关系

3.3.2 讲整个文件,这一段钻到最底下。 3.3.4 回到中间那一层,讲每条流的档案。

一、帧段比包段多了什么

包段和帧段有重叠,但帧段多了几项关键信息:

字段 含义 包段有吗


media_type 这帧是视频还是音频 有(codec_type) pict_type I/P/B 帧类型 没有 key_frame 是不是关键帧 有(在 flags 里) width / height 这一帧的分辨率 没有 pkt_size 它来自多大的包 有(size)

第二行和第四行是这一层独有的:只有真解出来了,才知道这是什么帧、多大。 包里只有「压缩后的一坨字节」,帧里才有「解压后的画面」,这是本质区别。

这个区别在排障时特别有用: 包段有内容而帧段是空的,说明容器没问题、解码器出问题了。 这时候换播放器没用,要换 ffmpeg 构建或者补那个解码器。

二、I/P/B 三种帧

三种帧的依赖关系决定了它们的体积和用途:

类型 依赖谁 特点


I 不依赖任何帧 能独立解,体积最大 P 参考前面的帧 体积中等,要按顺序解 B 前后都要参考 体积最小,但顺序最麻烦

「体积」那一列是理论上的排序,实测不一定符合—— 素材太短、画面太简单时,第一个 I 帧可能比后面的 B 帧还小。 要看出真正的差距,得用上百帧、有真实画面的素材。

最后一行那个「顺序最麻烦」就是 dts 出现负数的原因: B 帧要等后面的参考帧先到,所以它的解码顺序被提前了。

三、这一层的代价

-show_packets 只要顺着容器读,不解码;-show_frames 必须把每一帧都解出来。 所以前者的耗时跟文件大小有关,后者的耗时跟「帧数 × 单帧解码时间」有关。

参数 要读什么 耗时量级


-show_format 容器头 毫秒级 -show_streams 容器头 毫秒级 -show_packets 全部包,不解码 秒级(看大小) -show_frames 全部帧,要解码 分钟级(看分辨率)

所以排障顺序应该是从便宜到贵:先 format,再 streams,再 packets,最后 frames。 一上来就跑 frames,等十分钟才发现「其实 duration 是 N/A」,纯浪费时间。

还有一个参数值得知道:-count_frames。 它专门用来数准确帧数,因为容器里记的 nb_frames 常常不准或者没有。

四、动手 Demo:最细的一层,也最慢

把一份素材全部解出来,逐帧看类型、尺寸、来自多大的包。 重点是看清 I/P/B 的分布,以及「最大帧不一定是我以为的那种」。

import av

c = av.open(u"av3.mp4")
cc = c.streams.video[0].codec_context
PT = av.video.frame.PictureType

rows = []
for pk in c.demux(video=0):
    if not pk.size:
        continue
    for f in cc.decode(pk):
        rows.append((f.pts, PT(f.pict_type).name,
                     f.width, f.height, pk.size))
c.close()

for i, (p, t, w, h, s) in enumerate(rows[:8], 1):
    print(u"%d  pts=%s  %s  %dx%d  %d 字节" % (i, p, t, w, h, s))

cnt = {}
for r in rows:
    cnt[r[1]] = cnt.get(r[1], 0) + 1
print(u"帧数 %d,类型分布 %s" % (len(rows), cnt))

# 最大帧是哪一类
big = max(rows, key=lambda x: x[4])
print(u"最大的一帧 %d 字节,类型 %s" % (big[4], big[1]))

实跑输出:

--------------------------------------------------------------------
ffprobe -show_frames——最细的一层,也最慢
--------------------------------------------------------------------

**① 帧段比包段多了什么**

  字段               含义                   包段有吗
  -----------------------------------------------------------------
  media_type         这帧是视频还是音频     有(叫 codec_type)
  pict_type          I/P/B 帧类型         没有
  key_frame          是不是关键帧           有(在 flags 里)
  width / height     这一帧的分辨率         没有
  pkt_size           它来自多大的包         有(叫 size)

**第三、四行是这一层的价值:只有真正解出来了,才知道这是什么帧、多大。**
**包里只有「压缩后的一坨字节」,帧里才有「解压后的画面」,这是本质区别。**

**② 这也是它慢的唯一原因**

**`-show_packets` 只要顺着容器读,不解码;`-show_frames` 必须把每一帧都解出来。**
**所以前者在几百毫秒内跑完,后者要按帧数 × 解码时间来算。**

**本机这段素材 23 帧,全部解完拿到的是:**

  序     pts      帧类型     宽x高      来自多少字节
  ------------------------------------------------------
  1      0        I          160x90     57
  2      512      B          160x90     46
  3      1024     B          160x90     60
  4      1536     B          160x90     189
  5      2048     P          160x90     27
  6      2560     P          160x90     13
  7      3072     B          160x90     30
  8      3584     B          160x90     47

  帧类型     数量     说明
  ----------------------------------------
  I          1        关键帧,能独立解
  P          6        参考前面的帧
  B          16       前后都要参考

**这一段里 B 帧最多(16 个),I 最少(1 个),这是编码器默认的取舍。**
**I 帧独立但费字节,P 帧省字节但要依赖前面——这就是「关键帧间隔」在调的东西。**

**但注意本机这一行数据:最大的 I 帧 57 字节,最大的 B 帧 189 字节,**
**B 帧反而更大。这不是反例——因为这段素材只有 23 帧、画面还极简单,**
**第一个 I 帧几乎是空画面。真正拉开差距要上百帧的片子,那时候 I 帧一定是大头。**

**③ 反过来说,帧段也是判断「能不能解」的唯一途径**

**如果 `-show_packets` 有内容而 `-show_frames` 是空的,问题一定在解码器上。**
**这种时候换播放器没用,要换 ffmpeg 构建或者补那个解码器。**

注意 pict_type 返回的是数字,要用 PictureType 转成名字。 直接用数字会打出 1、2、3,看着莫名其妙——那分别是 I、P、B。

类型分布那一行说明这段素材用了多少 B 帧。 B 帧越多压缩率越高,但解码越麻烦、延迟越大——这是编码器在权衡。

最后一行是个反直觉的检查:最大的一帧不一定是 I 帧。 在这段极简素材上,第一个 I 帧几乎是空画面,反而比后面的 B 帧小。 要看出「I 帧最大」这个规律,得用有真实画面、上百帧的素材。

这也是本课反复强调的一条:素材太小时,很多「理论规律」看不出来。 遇到这种情况要老实写「这一份测不出来,原因是……」,不要硬编一个符合理论的结论。

30 秒实验

  1. 数一次:ffprobe -show_frames -select_streams v 片子.mp4 | grep -c pict_type=I,数 I 帧个数。
  2. 数准帧数:ffprobe -count_frames -select_streams v -show_entries stream=nb_read_frames 片子.mp4。
  3. 比快慢:同一个文件分别跑 -show_packets 和 -show_frames,用 time 量两者耗时。

常见误区

  1. 一上来就跑 -show_frames。它最慢,前面的层能查清的问题不该花这个时间。
  2. 以为 nb_frames 一定准。容器记的这个数常常偏或者干脆没有,要准就加 -count_frames。
  3. 以为 I 帧一定最大。短素材、简单画面上不一定。判断要基于实测,不要基于理论排序。
自测三问(点开对答案) 1. 帧段比包段多了哪两项关键信息? —— `pict_type`(I/P/B 类型)和 `width/height`(这一帧的分辨率)。这两项只有解码后才知道。 2. 为什么 `-show_frames` 比 `-show_packets` 慢那么多? —— 因为前者要解码每一帧,后者只顺着容器读包不解码。慢的是解码运算,不是读取。 3. 容器里记的 nb_frames 为什么不可靠? —— 它只是个「声明的值」,录制中断、拼接、转封装都可能让它偏。要准就得 `-count_frames` 真的数一遍。

相关笔记