pts 是「刻度数」,不是秒。同一个时刻在不同时间基下是不同的数。 所以跨时间基比 pts,等于比两种货币的数字却不看汇率。
上一节反复说「要配时间基」,这一节把时间基讲透。 换算公式只有一句,但漏一步就全错——下一节和再下一节各给一个实测。
一、四层时间基,一层都不能混
| 层 | 时间基 | 一秒多少刻度 | 谁定 |
|---|---|---|---|
| 容器时长 | 1/1000000 | 1000000 | FFmpeg 写死 |
| MP4 视频轨 | 1/15360 | 15360 | 容器 |
| 编码器 | 1/30 | 30 | 帧率 |
第一行和第二行不是一回事:container.duration 的单位是微秒,和视频轨的 1/15360 差了 65 倍。
所以算时长必须写清说的是哪一层——c.duration 是微秒,stream.duration 才是轨自己的刻度。
编码器那一行还有个坑:刚建完编码器去读 time_base 拿到的是 None,
必须等它真的打开了才有值。
实测就是这么个结果:add_stream 之后读是 None,调 start_encoding() 之后读到 1/30。
二、同一个时刻,四种写法
| 时刻 | 1/30 | 1/15360 | 1/90000 | 1/1000000 |
|---|---|---|---|---|
| 0.000 秒 | 0 | 0 | 0 | 0 |
| 0.500 秒 | 15 | 7680 | 45000 | 500000 |
| 1.000 秒 | 30 | 15360 | 90000 | 1000000 |
0.5 秒在 1/30 里是 15,在 1/15360 里是 7680——同一个时刻的两个完全不同的数。 这也是为什么「pts 直接比较大小」只在同一时间基下成立。
三、换算只有一句公式
新值 = 旧值 × 旧时间基 ÷ 新时间基,四舍五入。
C 接口里这句就是 av_rescale_q(pts, 旧tb, 新tb),内部用 64 位整数算,
不会像浮点那样丢精度。
PyAV 没把 av_rescale_q 暴露出来,
所以本文的 Demo 都是int(round(pts * 旧 / 新)) 自己算——原理一样,只是少了整数保护。
四、时间基选多细,决定时间戳多准
| 存法 | 一帧刻度 | 取整当步长 | 一小时偏多少 |
|---|---|---|---|
| 1/1000(毫秒) | 33.33 | 33 | 36.0 秒 |
| 1/90000(TS) | 3000 | 3000 | 0 秒 |
| 1/15360(MP4) | 512 | 512 | 0 秒 |
36 秒是这么算出来的:30 fps 一小时 108000 帧,每帧少 1/3 毫秒, 108000 ÷ 3 = 36000 毫秒 = 36 秒。 这就是 TS 用 1/90000 而不是 1/1000 的原因——它不是随便定的, 是「60 fps 的整数倍,还尽量细」。
注意这个偏差只在「拿整数当步长乘帧数」时累积。 如果你的代码是「每帧 pts 单独四舍五入」,误差不会累积——但很多老代码就是拿 33 毫秒当步长乘的。
五、动手 Demo:时间基——pts 是「刻度数」,不是「秒」
Demo 先读出四层时间基,再把同一个时刻换算成不同基下的值。 最后用一个反例算出「选错时间基,一小时偏 36 秒」这笔账。
c = av.open("in.mp4")
print("容器时长", c.duration) # 微秒
print("视频轨时间基", c.streams.video[0].time_base)
o = av.open("t.mp4", "w")
st = o.add_stream("libx264", rate=30)
print("编码器时间基", st.codec_context.time_base) # None!
o.start_encoding()
print("open 之后", st.codec_context.time_base) # 1/30
实跑输出:
--------------------------------------------------------------------
时间基——pts 是「刻度数」,不是「秒」
--------------------------------------------------------------------
**同样一个「第 1 秒」,在不同地方存的数完全不一样。**
**这就是所有人第一次读 ffprobe 输出时的困惑。**
**四层各有各的时间基,一层都不能混:**
层 时间基 一秒多少刻度 谁定
------------------------------------------------------------------
容器时长 1/1000000 1000000 FFmpeg 写死
MP4 视频轨 1/15360 15360 容器
编码器 1/30 30 帧率
**注意第一行和第二行不是一回事**:`container.duration` 的单位是微秒,
和视频轨的 `1/15360` 差了 65 倍。
**这就是为什么算时长必须写清说的是哪一层**——`c.duration` 是微秒,
`stream.duration` 才是轨自己的刻度。
**编码器那一行还有个坑**:刚 `add_stream` 完去读 `time_base` 拿到的是 `None`,
**必须等编码器真的打开了才有值**。
这次是调 `start_encoding()` 逼它打开,
之后读到 `1/30`——**这就是「原来没开就取不到」的实证,不是猜的。**
**同一个时刻,换几种时间基各自要写多少:**
时刻 1/30 1/15360 1/90000 1/1000000
--------------------------------------------------------------
0.000 秒 0 0 0 0
0.500 秒 15 7680 45000 500000
1.000 秒 30 15360 90000 1000000
1.033 秒 31 15872 93000 1033333
**0.5 秒在 1/30 里是 15,在 1/15360 里是 7680**——同一个时刻的两个完全不同的数。
**所以「pts 的大小」只有配上同一个时间基才能比**,跨时间基比 pts 等于比两种货币的数字不看汇率。
**换算公式只有一句:**
**新值 = 旧值 × 旧时间基 ÷ 新时间基**,四舍五入。
C 接口里这句就是 `av_rescale_q(pts, 旧tb, 新tb)`,它内部用 64 位整数算,
**不会像浮点那样丢精度**。
**PyAV 没有把 `av_rescale_q` 暴露出来**,
所以本文所有 Demo 都用 `int(round(pts * 旧 / 新))` 自己算,**原理一样,只是少了整数保护**。
**顺手验一下「四舍五入会不会累积误差」**:
从 1/30 换到 1/90000 再换回来,看能不能原样还原:
项 值
--------------------------------------------------
换过去又换回来,还原失败的帧 0 个
最大偏差 0 个刻度
**这次是 0,因为 90000 能被 30 整除**,每一步都是整数,换回来当然还是原值。
**真会出问题的是选了个除不尽的基**,比如有人图省事用 1/1000(毫秒):
30 fps 一帧是 33.33 个刻度,**每帧都得砍掉 0.33 毫秒**。
**这个砍法是会累积的**,因为它不是「每帧四舍五入」而是「拿 33 当步长乘帧数」:
存法 一帧刻度 取整当步长 一小时偏多少
--------------------------------------------------------------
1/1000(毫秒) 33.33 33 36.0 秒
1/90000(TS) 3000 3000 0 秒
1/15360(MP4) 512 512 0 秒
**36 秒是这么算出来的**:30 fps 一小时 108000 帧,每帧少 1/3 毫秒,
108000 ÷ 3 = 36000 毫秒 = 36 秒。
**这就是 TS 用 1/90000 而不是 1/1000 的原因**。
**它不是随便定的,是「60 fps 的整数倍,还尽量细」。**
同一个 time_base 属性,open 前后读到两个不同的东西(None 和 1/30),
这就是「没开就取不到」的实证。
换算表里 1.033 秒那一行(31/15872/93000/1033333)是故意加的:
它证明「时刻」本身带小数,取整会花掉几个刻度。
30 秒实验
- 把 1.0333333 秒换算到 1/1000,看四舍五入成多少,误差多大。
- 用
float(c.duration) / 1000000算时长,和你以为的秒数比。 - 把编码器
rate改成 25,看它的时间基变成多少。
常见误区
- 以为 pts 的单位是毫秒。 它是「时间基的刻度数」,不同地方基不一样。
- 以为
container.duration和stream.duration一个单位。前者是微秒,后者是轨的刻度。 - 以为时间基随便选个细的就行。 选错会累积偏差,1/1000 存 30 fps 一小时偏 36 秒。
自测三问(点开对答案)
1. 0.5 秒在 1/30 和 1/15360 下分别是多少? —— 15 和 7680。**同一个时刻,两个数差 512 倍。** 2. 换算公式是什么? —— 新值 = 旧值 × 旧时间基 ÷ 新时间基,四舍五入。C 里是 `av_rescale_q`。 3. 为什么编码器的时间基刚建完读不到? —— 编码器还没打开,**时间基要等 `avcodec_open2` 之后才定下来**。下一节用实测告诉你「不换算会错成什么样」——转封装换时间基。
- ☐ (本节完,下一节 04.06.03 转封装过程中的时间基转换)