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