转封装换容器时,时间基往往跟着变,包上的所有刻度数都要换。 不换算的文件照样能打开、画面照样动,就是节奏全乱——这种错最阴。
上一节讲了公式,这一节把它用起来。 04.05.04 的转封装之所以不用换算,是因为输出照抄了输入的时间基。 这一节故意把输出轨改成 1/90000,然后三条路并排走。
一、三条路,差多少
| 文件 | 时间基 | 末包 pts | 读回时长 |
|---|---|---|---|
| 原片 | 1/15360 | 14848 | 1.00 秒 |
| 全程不换 | 1/90000 | 14848 | 0.17 秒 |
| 只换 pts/dts | 1/90000 | 87000 | 0.97 秒 |
| 连 duration 一起换 | 1/90000 | 87000 | 1.00 秒 |
「全程不换」是灾难现场:pts 还是 0、512、1024 那几个数,但输出轨的时间基已经变成 1/90000, 同一个 512 从「1/30 秒」变成了「5.7 毫秒」,时长只剩原来的 17%。
「只换 pts/dts」看着快对了,其实还差 0.03 秒。pts 全对上了,但每包还带着自己的 duration, 末帧那个 512 没换,512 刻度在 1/90000 里只有 5.7 毫秒,本该是 3000 刻度(33 毫秒)。 这 0.03 秒的缺口,就是「pts 换对了、duration 忘了换」的铁证。
二、要换的一共四处
| 序号 | 要换的东西 | 漏了会怎样 |
|---|---|---|
| ① | packet.pts |
播放位置全错 |
| ② | packet.dts |
dts 可能大于 pts,解码器直接报错 |
| ③ | packet.duration |
总时长差一帧,越往后越明显 |
| ④ | packet.time_base |
数值对、基不对,等于全错 |
前两条是所有人都记得的,后两条是所有人都会忘的。 ③ 只影响总时长、不影响画面,所以最难发现——你看着画面正常,一查时长少了半秒。
记住一个判断标准:只要时间基变了,包上所有「以刻度为单位的数」都要换。pts、dts、 duration 一个不漏。
三、为什么内容一个字节没动
这 30 个包的大小、内容一个字节没改,换的只是那几个数字。 所以这种错误最阴:文件能打开、画面能播、拖动也能拖,就是「快进一小时只走了十分钟」。
排查办法:读回来算「末包 pts ÷ 时间基分母」,和预期的秒数比。 差一个固定倍数就是没换算,差一点点就是 duration 的问题。
四、什么时候必须换
| 场景 | 要不要换 | 说明 |
|---|---|---|
| MP4 → MP4,同参数 | 不用 | 照抄模板,基不变 |
| MP4 → TS | 要 | TS 用 1/90000 |
| MP4 → MKV | 要 | MKV 用 1/1000 |
| TS → MP4 | 要 | 1/90000 → 1/15360 |
判据很简单:in_stream.time_base == out_stream.time_base 就跳过换算,不等就必须换。
别「反正都换一遍」——换两次等于没换,而且更难查。先判断,再决定换不换。
五、动手 Demo:转封装换时间基,不换算会错成什么样
Demo 生成三个文件:全程不换、只换 pts/dts、连 duration 一起换。 三个文件的内容完全一样,只有时间戳不同,读回来的时长差得很明显。
def build(path, mode):
c = av.open("in.mp4")
s = c.streams.video[0]
o = av.open(path, "w")
st = o.add_stream_from_template(s)
st.time_base = Fraction(1, 90000) # 故意换基
for pk in c.demux(s):
if mode in ("pts", "all"):
pk.pts = rescale(pk.pts, tb_in, tb_out)
pk.dts = rescale(pk.dts, tb_in, tb_out)
if mode == "all":
pk.duration = rescale(pk.duration, tb_in, tb_out)
pk.time_base = tb_out
o.mux(pk)
实跑输出:
--------------------------------------------------------------------
转封装换时间基,不换算会错成什么样
--------------------------------------------------------------------
**上一节那个 remux(Demo 57)之所以不用换算,是因为输出照抄了输入的时间基。**
**这一节故意把输出轨的时间基改成 1/90000(MPEG-TS 的常用值)。**
**然后三条路并排走:全程不换、只换 pts/dts、连 duration 一起换。**
**四个文件摆一起:**
文件 时间基 首包 pts 末包 pts 读回时长
----------------------------------------------------------------------
原片 1/15360 0 14848 1.00 秒
全程不换 1/90000 0 14848 0.17 秒
只换 pts/dts 1/90000 0 87000 0.97 秒
连 duration 一起换 1/90000 0 87000 1.00 秒
**「全程不换」那一行是灾难现场**:pts 还是 0、512、1024 那几个数,
但输出轨的时间基已经变成 1/90000。
**同一个 512 从「1/30 秒」变成了「5.7 毫秒」**,时长只剩原来的 17%。
**「只换 pts/dts」那一行看着快对了,其实还差 0.03 秒**:30 帧的 pts 全对上了,
但**每包还带着自己的 duration**,末帧那个 512 没换,512 刻度在 1/90000 里只有 5.7 毫秒,
本该是 3000 刻度(33 毫秒)。
**这 0.03 秒的缺口,就是「pts 换对了、duration 忘了换」的铁证。**
**「连 duration 一起换」才和原片的 1.00 秒严丝合缝。**
注意这 30 个包的大小、内容一个字节没动,**换的只是那几个数字**。
所以这种错误最阴——文件能打开,画面也能动,就是节奏全乱。
**转封装要换的一共四处,漏一处错一处:**
序号 要换的东西 漏了会怎样
------------------------------------------------------------------
① `packet.pts` 播放位置全错
② `packet.dts` dts 可能大于 pts,解码器直接报错
③ `packet.duration` 总时长差一帧,越往后越明显
④ `packet.time_base` 数值对、基不对,等于全错
**前两条是所有人都记得的,后两条是所有人都会忘的。**
**记住一个判断标准:只要时间基变了,包上所有「以刻度为单位的数」都要换**,pts、dts、
duration 一个不漏。
三行读回时长(0.17/0.97/1.00 秒)就是本节的全部证据:少换一处,就少一点时间。 注意 0.17 秒这个数不是「压成了 1/5」,是 17%——15360 ÷ 90000 ≈ 0.17。 报比例前先算清方向,别把 6 倍说成 5 倍。
30 秒实验
- 只换 pts 不换 dts,看重跑时 mux 会不会报错。
- 把输出轨改成 1/1000(MKV 常用),重跑,看三个时长各是多少。
- 把
pkt.time_base那一行删掉,看会怎样——数值对、基不对的结果。
常见误区
- 以为 pts 数值对了就行。 时间基不对,等于全错,而且文件照样能打开。
- 以为
duration不用换。 它也是刻度数,漏了总时长就差一帧,越往后越明显。 - 以为「反正都换一遍」最保险。 换两次等于没换,要先判断两个基是否相同。
自测三问(点开对答案)
1. 为什么「只换 pts」还会差 0.03 秒? —— **每包还带着自己的 duration**,末帧那个没换,512 刻度在 1/90000 里只有 5.7 毫秒, 本该是 33 毫秒。 2. 转封装要换哪四处? —— `pts`、`dts`、`duration`、`time_base`。 **前两个影响播放位置,第三个影响总时长,第四个决定前三个怎么解释。** 3. 怎么快速判断「是不是忘了换算」? —— 读回来算「末包 pts ÷ 时间基分母」和预期秒数比。**差固定倍数就是没换算。**下一节换到转码:帧上的 pts 怎么改,为什么改错了会「全塌成一个点」。
- ☐ (本节完,下一节 04.06.04 转码过程中的时间基转换)