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

packets 是容器切出来的段,不是画面。 首包 pts 是不是 0、关键帧有几个,这两条能定位大半时间戳问题。

📝 本节和前面几节的关系

3.3 讲了三段结构,这一段开始逐段细讲,先讲包这一层。 3.3.2 讲整个文件(format),3.3.3 讲最细的帧。

一、一个包带哪些字段

这一层的字段比 format 多,但常用的就六个:

字段 含义 看它能发现什么


codec_type 视频还是音频 有没有混进不认识的流 pts / dts 显示/解码时间戳 顺序乱不乱、从几开始 duration 这个包覆盖多久 有没有异常长的包 size 字节数 关键帧是不是特别大 pos 在文件里的偏移 索引对不对 flags K 关键帧/D 可丢 哪里能切

pts 和 dts 的区别是这一层的难点:pts 是「什么时候显示」,dts 是「什么时候解码」。 有 B 帧时两者不一样,因为 B 帧要等后面的参考帧先解出来。

所以 dts 出现负数是正常的,不是文件坏了。 它表示「这几帧的解码时刻被排在 0 之前」——只是因为显示时刻还在后面。

二、首包 pts 是最该看的一项

拿到一个「播放卡在开头几秒」的文件,第一件事就是看首包 pts:

首包 pts 说明 怎么修


0 正常 不用管 很小的正数 可能有一段起始偏移 重封装试试 很大的数 时间戳没有归零 -c copy 重封装

第三行是很经典的一类问题:录屏或抓流出来的文件,时间戳是从「开机时刻」开始算的。 播放器假设第一帧是 0,拿到一个大数就开始算错,表现是开头卡住、进度条乱跳。

修法就是重封装一次:ffmpeg -i 坏.mp4 -c copy 好.mp4。 码流一个字节不改,但容器会重新排时间戳,从 0 开始。这是最省事的修法。

三、关键帧数决定「能切几刀」

flags 里带 K 的就是关键帧,它决定了两件事:

事 为什么靠关键帧 影响


随机访问 只有关键帧能独立解 进度条能不能拖 无损剪切 切口必须在关键帧上 切出来会不会花

第一行解释了「为什么有些片子拖进度条很卡」:关键帧太稀,每次拖都要从远处解起。 第二行解释了「为什么剪切会花屏」:切口落在非关键帧上,后面的帧缺参考。

所以想无损剪切又不出问题,只有两个办法: 一是只在关键帧处切(切口不精确),二是重编码(切得准但要重压)。

四、动手 Demo:看见容器里的每一次切分

把一份 5 秒素材包这一层的信息全打出来:前八个包的字段、整体统计、与帧数的对比。 重点是看清「包」和「帧」不是一回事。

import av

c = av.open(u"src.mp4")
tb = c.streams.video[0].time_base.denominator

rows = []
for pk in c.demux(video=0):
    if not pk.size:
        continue
    rows.append((pk.pts, pk.dts, pk.size, pk.is_keyframe))
    if len(rows) >= 8:
        break
c.close()

print(u"时间基 1/%d" % tb)
for i, (p, d, s, k) in enumerate(rows, 1):
    print(u"%d  pts=%s  dts=%s  %d 字节  %s"
          % (i, p, d, s, u"关键帧" if k else u""))

# 整体统计
c = av.open(u"src.mp4")
npk = tot = big = key = 0
first = None
for pk in c.demux(video=0):
    if not pk.size:
        continue
    if first is None:
        first = pk.pts
    npk += 1
    tot += pk.size
    big = max(big, pk.size)
    key += 1 if pk.is_keyframe else 0
nf = sum(1 for _ in c.decode(video=0))
c.close()
print(u"包 %d 个 合计 %d 字节 最大 %d 关键帧 %d" % (npk, tot, big, key))
print(u"首包 pts=%s  解出 %d 帧" % (first, nf))

实跑输出:

--------------------------------------------------------------------
ffprobe -show_packets——看见容器里的每一次切分
--------------------------------------------------------------------

**① 一个包带哪些字段**

  字段           含义                 看它能发现什么
  ------------------------------------------------------------
  codec_type     视频还是音频         有没有混进不认识的流
  pts / dts      显示/解码时间戳     顺序乱不乱、从几开始
  duration       这个包覆盖多久       有没有异常长的包
  size           字节数               关键帧是不是特别大
  pos            在文件里的偏移       索引对不对
  flags          K 关键帧/D 可丢     哪里能切

**pts 和 dts 的区别是这一节的难点:pts 是「什么时候显示」,dts 是「什么时候解码」。**
**有 B 帧的时候两者不一样,因为 B 帧要等后面的帧先解出来。**

**② 本机素材前 8 个包,原样打出来**

  序     pts      dts       字节     关键帧
  ---------------------------------------------
  1      0        -1024     731      K
  2      512      -512      45       -
  3      1024     0         59       -
  4      1536     512       52       -
  5      2048     1024      59       -
  6      2560     1536      49       -
  7      3072     2048      54       -
  8      3584     2560      48       -

**注意 pts 那一列不是 0、1、2 这样递增的,它是按时间基换算出来的时间值。**
**这份素材的时间基是 1/12800,每帧涨 512,正好对应每秒 25 帧。**

**第一行的 dts 是负数(-1024),这不是错——B 帧要等后面的参考帧先解,**
**所以它的「解码时刻」被排到了 0 之前。看到负 dts 不用慌,那是正常的。**

**③ 整段素材的包级统计**

  项           值       说明
  ---------------------------------------------
  包数         125      容器切出来的段数
  合计字节     7696     不含容器开销
  最大包       731      通常是关键帧
  关键帧数     5        能切的位置就这么多
  首包 pts     0        不从 0 开始就有问题

**最后一行是排障最常用的一条:首包 pts 不是 0,播放器就可能卡在开头几秒。**
**修的办法是先跑一次 `-c copy` 重封装,让时间戳重新从 0 排。**

**④ 包数不等于帧数**

**这一段解出 125 帧,容器里切出 125 个包。两者接近但不保证相等。**
**包是容器的单位、帧是画面的单位,中间差的就是「一个包能不能装下多帧」这件事。**

pts 那一列不是 0、1、2,而是按时间基换算的时间值。 时间基 1/12800、每帧涨 512,正好等于每秒 25 帧——这个换算是理解 pts 的关键。

dts 出现负数是 B 帧造成的,不是文件坏了。 它表示解码时刻被提前了,为的是让显示时刻能排到正确的位置。

最后一行「包数」和「帧数」要对比着看。 这一段素材两者恰好相等,但含 B 帧的片子上包数会多一点(有零长度的结束标记)。 所以不能拿包数当帧数用,也不能反过来。

首包 pts 是 0,说明这份文件是「健康的」—— 录屏、抓流出来的文件常常不是 0,那时候就该重封装一次。

30 秒实验

  1. 看前几个包:ffprobe -show_packets -select_streams v 片子.mp4 | head -40,只看开头几个包。
  2. 只看关键帧:ffprobe -show_packets -select_streams v 片子.mp4 | grep -c flags=K,数关键帧个数。
  3. 看首包:ffprobe -show_packets -select_streams v 片子.mp4 | head -20,确认 pts 是不是 0。

常见误区

  1. 把包数当帧数。包是容器的段,帧是画面的单位,两者不保证相等,含 B 帧时包数会多。
  2. 看到 dts 是负数就以为文件坏了。B 帧本来就会让解码时刻提前,负 dts 是正常的。
  3. 以为剪切可以在任意位置无损做。想不重编码就只有关键帧处能切,其余位置切必花屏。
自测三问(点开对答案) 1. pts 和 dts 分别是什么? —— pts 是显示时刻(presentation),dts 是解码时刻(decoding)。有 B 帧时两者不同,dts 可能为负。 2. 首包 pts 是个很大的数,会有什么表现?怎么修? —— 播放器按 0 开始算会算错,表现是开头卡住、进度条乱跳。修法是 `-c copy` 重封装一次,让时间戳归零。 3. 为什么关键帧太少会让拖进度条变卡? —— 因为播放器只能从关键帧开始解码,关键帧稀就得从很远的地方解起,才能到你要的那个位置。

相关笔记