转码时 pts 挂在帧上,不是包上。
改 pts 必须同时改 frame.time_base,只改一半等于换算两遍——全塌成一个点。
上一节换的是包,这一节换的是帧。错的地方不同,症状一模一样。 这一段有个和 C 语言的分工差异,书上的写法照搬到 PyAV 会出错,必须说清。
一、帧自带时间基
解码出来的帧,frame.time_base 是解码器填好的(这次是 1/15360),而编码器要的是 1/30。
| 项 | 值 |
|---|---|
| 解码器所在的流时间基 | 1/15360 |
| 帧自带的 time_base | 1/15360 |
| 编码器要的时间基 | 1/30 |
第一行和第三行不一样,所以直接搬 pts 是错的。
第二行是关键:AVFrame 上没有时间基这个字段,但 PyAV 在帧上带了一个,它会按这个基去换算。
二、三种写法,只有一种错
| 写法 | 改了什么 | 读回时长 | 对不对 |
|---|---|---|---|
| 原样送进去 | 什么都不改 | 1.00 秒 | 对 |
| 只改 pts | fr.pts = 换算值 |
0.03 秒 | 错 |
| pts + time_base 一起改 | 两个都换成 1/30 | 1.00 秒 | 对 |
第三种(只改 pts)为什么崩,得看它的 pts 变成了什么:首包和末包都是 0, 所有帧的时间戳塌成一个点。 原因是换算算了两遍:你把 512 改成 1(按 1/30 算过一遍),但帧上还挂着 1/15360, PyAV 又按 1/15360 把它折回 1/30,1 ÷ 15360 秒再乘 30 = 0.002 个刻度,取整就是 0。
所以「改 pts 必须同时改 time_base」是 PyAV 的规矩,缺一半就等于算两次。
三、C 和 PyAV 的分工不同
| 环境 | 帧上有时间基吗 | 谁负责换算 |
|---|---|---|
| C(书里的写法) | 没有 | 你自己 av_rescale_q |
| PyAV(本文) | 有,解码器填的 | 库按它换,你也别乱改 |
这不是谁对谁错,是两套约定:C 里时间基挂在 codec context 上,帧只是一堆裸数据; PyAV 为了少踩坑,把时间基塞进了帧。 看懂这一层,就不会被「书上说要换算,我照做反而错了」这种事搞懵。
四、固定套路,背下来
| 步 | 做什么 |
|---|---|
| ① | 记下输入流时间基 itb |
| ② | 给输出流设时间基 otb |
| ③ | 每帧 frame.pts = av_rescale_q(frame.pts, itb, otb) |
| ④ | 转封装同一套,作用在 packet.pts 和 packet.dts 上 |
最省事的其实是「什么都不改」:原样送进去,库会按帧上带的时间基自己换。 前提是你别去动它——一旦手动改 pts,就得连 time_base 一起改。
这一章从头到尾其实只讲了一件事:时间戳是个「数 × 基」的组合,动了一半就等于全错。
五、动手 Demo:转码换时间基,改动 pts 时别把时间基漏了
Demo 先查帧自带的时间基,再用三种写法各生成一个文件,比读回时长。 三种写法的区别只有两行代码,结果却是 1.00 秒和 0.03 秒的差别。
for fr in c.decode(s):
src = fr.pts # 单位是 1/15360
if mode == "shift":
fr.pts = rescale(src, tb_in, Fraction(1, 30))
elif mode == "both":
fr.pts = rescale(src, tb_in, Fraction(1, 30))
fr.time_base = Fraction(1, 30) # 这一行是关键
for pk in st.encode(fr):
o.mux(pk)
实跑输出:
--------------------------------------------------------------------
转码换时间基,改动 pts 时别把时间基漏了
--------------------------------------------------------------------
**转码(transcode)和转封装不一样**:中间要解码成帧、再编码成新包。
**帧上的 pts 是解码器给的,编码器要的是自己时间基下的数。**
**查一下解码出来的帧,它自带时间基:**
项 值
--------------------------------------------------
解码器所在的流时间基 1/15360
帧自带的 time_base 1/15360
编码器要的时间基 1/30
**第一行和第三行不一样**,所以直接搬 pts 是错的。
**第二行是关键**:`AVFrame` 上没有时间基这个字段,但 PyAV 在帧上带了一个 `time_base`,
**它会按这个基去换算**。
**三种写法,各生成一个文件:**
写法 改了什么 读回时长 对不对
----------------------------------------------------------------------------
原样送进去 什么都不改 1.00 秒 对
只改 pts `fr.pts = 换算值` 0.03 秒 错
pts + time_base 一起改 两个都换成 1/30 1.00 秒 对
**原片的读回时长是 1.00 秒**,拿它当基准。
**第三种(只改 pts)为什么会崩,得看它的 pts 变成了什么:**
文件 时间基 首包 pts 末包 pts
--------------------------------------------------------------
原片 1/15360 0 14848
只改 pts 1/15360 0 0
两个都改 1/15360 0 14848
**「只改 pts」那一行的首包和末包都是 0**,所有帧的时间戳塌成一个点,时长只剩 0.03 秒。
**原因是换算算了两遍**:你把 512 改成了 1(按 1/30 算过一遍),但帧上还挂着 1/15360,
PyAV 又按 1/15360 把它折回 1/30,
**1 ÷ 15360 秒再乘 30 = 0.002 个刻度,取整就是 0**,全塌了。
**所以「改 pts 必须同时改 time_base」是 PyAV 的规矩**,缺一半就等于算两次。
**C 语言里没有这个坑,因为 `AVFrame` 根本没有 `time_base` 字段:**
环境 帧上有时间基吗 谁负责换算
--------------------------------------------------------------------
C(书里的写法) 没有 你自己 `av_rescale_q`
PyAV(本节) 有,解码器填的 库按它换,你也别乱改
**这不是谁对谁错,是两套约定**:C 里时间基挂在 codec context 上,帧只是一堆裸数据;
PyAV 为了少踩坑,把时间基塞进了帧。
**看懂这一层,就不会被「书上说要换算,我照做反而错了」这种事搞懵。**
**一句话总结本节:**
① 转封装:改 `packet.pts`/`dts`,同时改 `packet.time_base`
② 转码:改 `frame.pts`,同时改 `frame.time_base`
③ 不想改就别改——原样送进去,库会按帧上带的时间基自己换
**三条里最省事的是第三条,前提是你别去动它。**
「只改 pts」的末包 pts 是 0, 这就是「塌成一个点」的量化证据——不是时长算错了,是所有帧都挤在 0 时刻。 注意原样送进去那一行是对的:库自己按 1/15360 折到 1/30。 所以你越帮越忙的时候,症状就是「越过越错」。
30 秒实验
- 把
fr.time_base改成 1/90000 而不是 1/30,看还能不能对上。 - 用
frame.time属性(秒,浮点)代替 pts,看还有没有这个坑。 - 把编码器
rate改成 25,重跑三种写法,看结论会不会变。
常见误区
- 以为改 pts 就够了。 PyAV 还会按帧自带的时间基换一次,等于算两遍。
- 以为书上的
av_rescale_q照搬到 PyAV 就行。 C 的帧没有时间基,PyAV 的有,行为不一样。 - 以为时长 0.03 秒是「编码失败」。 编码成功了,是时间戳全塌到 0,容器只能给个最短时长。
自测三问(点开对答案)
1. 为什么只改 pts 会把所有帧压到 0? —— PyAV 又按帧上的 1/15360 把你改后的数折回 1/30,**1 ÷ 15360 × 30 ≈ 0.002,取整成 0**。 2. C 和 PyAV 在这件事上最大的差别是什么? —— C 的 `AVFrame` 没有时间基字段,**换算全归你**; PyAV 给帧带了时间基,**库会替你换,你改了 pts 就得连基一起改。** 3. 最省事的写法是什么? —— **什么都不改,原样送进编码器**,库按帧上的时间基自己换。下一章转到视频监控的另一半:ONVIF 与 GB/T 28181 协议。
- ☐ (本章完,下一节 05.01.01 视频监控系统的功能及特点)