🎬 第七章 AVFormat封装层理论及案例实战
📄 一句话概括

pts 只是个整数,乘上 time_base 才是秒。 同一个文件里三个刻度并存,混着比必错。

📝 本节和前面几节的关系

这是整个封装层最容易算错的一节,值得慢读。 7.5 存包会绕开这套刻度,7.6 转封装会改这套刻度。

一、三个刻度同时存在

容器一个,每条流各一个。 东西 常见刻度 一格多长


容器时长 1/1000000 一微秒 视频流 1/12800 万分之一万二千秒 音频流 1/44100 一个采样

为什么视频是 1/12800 这种怪数:它是把一秒切成能被常见帧率整除的份数。 12800 能被 25、50、100 整除,所以 25 帧的片子每帧正好 512 格。 音频用 1/44100 是因为采样率就是 44100,一个 pts 一格就是一个采样点,天然对齐。 容器用微秒是为了统一记账:不管里面有几条流、尺子多怪,总账一律换算成微秒。

二、换算只有一个公式

秒数 = pts × time_base,反过来 pts = 秒数 ÷ time_base。 已知 求 怎么做


pts + time_base 秒 相乘 秒 + time_base pts 相除 两条流的 pts 能不能比 先都换成秒

第三行是全部错误的来源。 视频 pts 是 512,音频 pts 是 1024,谁大? 没法比,因为格子不一样长。 换成秒之后:视频 0.04 秒,音频 0.023 秒。 结论与数字大小完全相反。 所以规矩就一句:碰到 pts 先问「这是哪条流的」,拿到它的 time_base 再算。

三、为什么用分数不用小数

不是怀旧,是精度。 帧率 小数 攒多久出问题


25 0.04 整数,不攒 30 0.0333… 一直攒 29.97 0.03336… 一小时差几帧

用整数 pts 加整数比 time_base,只要不溢出就是精确的。 这是拿「整数运算」换「永远不漂」。 29.97 这个怪帧率就是因为这个:NTSC 当年为了避开干扰,把 30 乘了 1000/1001。 用分数写就是 30000/1001,一个数都不丢。 所以看到 30000/1001 别以为是写错了,那是正宗的 29.97。

四、帧率是从哪来的

很多人以为容器里存了一个「帧率」字段。多数时候没有。 它是从相邻两个 pts 的间隔算出来的:间隔 512 格,512 × (1/12800) = 1/25 秒,倒数就是 25 帧。 所以可变帧率的视频,average_rate 只是个平均值,真实帧率逐段不同。 拿平均值去算时长,会差。 要靠 pts 精确算时长:最后一个 pts 减去第一个,再乘 time_base,加上最后一帧的 duration。

五、转封装会改刻度

这一点在 7.6 会亲眼看到。 容器 视频 time_base 音频 time_base


mp4 1/12800 1/44100 matroska 1/1000 1/1000 mpegts 1/90000 1/90000

每个容器有自己的规定刻度,mux 的时候 FFmpeg 会帮你换算 pts。 所以你看到的是 pts 数字变了、 秒数没变。 这时候要是只比 pts 不看 time_base,就会以为「时间全乱了」。 这是排查转封装问题时最常犯的错。 正确的对照方式:两边都换成秒,再比。多写一行除法,省半天怀疑。

六、动手 Demo:时基这根尺子,一格到底多长

先列出三个刻度,再把前五个包的 pts 换成秒。

import av

c = av.open("tiny.mp4")
vs = c.streams.video[0]
a = c.streams.audio[0]
print("容器用微秒", c.duration)
print("视频", vs.time_base, "音频", a.time_base)

for pk in c.demux(vs):
    if pk.size == 0:
        continue
    print(pk.pts, pk.pts * vs.time_base)
    if pk.pts > 1536:
        break
c.close()

实跑输出:

--------------------------------------------------------------------
时基这根尺子,一格到底多长
--------------------------------------------------------------------

**① 两个时间刻度并存**

  东西         时间刻度     一格多长
  ------------------------------------------
  容器时长     微秒         1/1000000 秒
  视频流       1/12800      1/12800 秒
  音频流       1/44100      1/44100 秒

**同一个文件里三个刻度不一样。** 容器用微秒算总账,视频按 12800 分之一秒数,
音频按 44100 分之一秒数。

**所以 `vs.duration` 与 `a.duration` 那两个数不能直接比**:一个是 **25600** 格,
一个是 **89224** 格,各自的格子长度不同。

**② 换算只有一个公式**

**秒数 = pts × time_base。** 反过来:**pts = 秒数 ÷ time_base**。

  pts      乘 time_base     等于
  --------------------------------------
  0        0/12800          0.000 秒
  512      512/12800        0.040 秒
  1024     1024/12800       0.080 秒
  1536     1536/12800       0.120 秒
  2048     2048/12800       0.160 秒

**相邻两个 pts 差 512,乘出来正好是 1/25 秒。**
这就是那个「25 帧」的来源:**帧率不是单独存的,是从 pts 间隔里算出来的。**

**③ 为什么不用小数**

**用小数会攒误差。** 1/25 = 0.04,看着干净;
可 1/30 = 0.0333…,一秒攒一点点,一小时就能差出好几帧。

**用分数就没有这个问题**:pts 是整数,time_base 是整数比,两者相乘再除,只要分子不溢出就是精确的。
**这是拿整数换精度。**

**④ 换算成「另一个刻度」要显式做**

**把视频的 pts 直接塞给音频用,时间会彻底错掉**,因为两把尺子不一样长。
要转就用 `av.time_base` 那一套换算,或者干脆两次都先换成秒再算。

**实践里的规矩很简单**:碰到 pts 先问「这是哪条流的」,拿到对应 time_base 再换算。
**问错一次,整段就偏了。**

pts 差 512,乘出来差 1/25 秒,帧率就是这么算出来的。 容器用微秒、视频用 1/12800、音频用 1/44100,三把尺子。

30 秒实验

  1. 把 vs.time_base 换成 a.time_base 去算视频的 pts,看差多少。
  2. 用 float(pk.time) 直接拿秒数,与手算的对一对。
  3. 算一遍 vs.duration * vs.time_base 与 container.duration,看差多少。

常见误区

  1. 拿 pts 直接比大小。 不同流的格子不一样长。
  2. 把 time_base 当小数。 它是分数,要用分数算。
  3. 用 average_rate 算总时长。 可变帧率下会差。
自测三问(点开对答案) 1. pts 与秒数怎么换算? —— 秒数 = pts × time_base。反过来 pts = 秒数 ÷ time_base。 2. 为什么 time_base 用分数不用小数? —— 小数会攒误差,分数只要不溢出就是精确的。 3. 转封装之后 pts 变了,时间真的变了吗? —— 没有。容器的刻度变了,秒数不变。

相关笔记