解码、滤镜、编码、封装四段串起来,文件就出来了。 但时长会是素材的几百倍——因为滤镜不替你换算 pts。
9.3 讲搭图,9.4.1 讲命令行,这一节用 API 把整条链落成文件。 这是第九章的收尾,也是全书第一次踩到「时间戳没对齐」的实战坑。
一、整条链跑完,账先记下
跑完先看五个数,不要急着看画面。 项 值
输出文件 filtered.mp4 帧数 50 包数 50 体积 约 1 万字节 读回来的时长 1003.56 秒
文件出来了、尺寸对了、包数也对——但时长大得离谱。 素材本身只有 2.02 秒,读回来却是一千多秒。 这种现象最容易被放过:播放器能播,画面也对,只有进度条不对。不看时长就永远发现不了。
二、坑在哪:三把尺子
同一串数字,换了尺子量,时间就差几百倍。 哪一环 用哪把尺子 说明
解码出来的帧 流的 time_base 比如 1/12800 编码器要的帧 编码器的 time_base 比如 1/25 容器里的时间 微秒 容器自己再换一次
滤镜出来的帧,pts 还是源码流的刻度,而编码器是照着 1/25 去理解的。 同一串数字从 1/12800 的尺子挪到 1/25 的尺子上,时间就被放大了几百倍。 关键在于:滤镜这一环不会替你换算 pts,它只管像素。 这是设计使然,不是 bug——它不知道你要把帧交给谁。 这就是 8.4 讲的「三把尺子」在实战里的样子。那节讲的是概念,这里看到的是它的后果。
三、修法:给帧重新编号
一行代码,按帧序号重编 pts。 做法 读回来的时长
不换算 pts 1003.56 秒 按帧号重编(o.pts = i) 2.00 秒
o.pts = i 的意思是「第 i 帧的显示时刻就是第 i 个单位」,配合编码器的 1/25,
50 帧正好 2 秒。
这一行是整个 9.4 最值钱的东西。 滤镜链跑通不算完,时间戳对齐才算完。
真实项目里更常见的写法是按时间算:o.pts = i * (源 time_base / 编码器 time_base),
这样变速、丢帧、补帧时才不会错。按帧号重编只适合「一帧不丢」的场合。
四、这套写法的复用价值
把滤镜那一段换掉,整条链就能干别的事。 换掉哪一段 变成什么
scale 换成 overlay 加水印 scale 换成 eq 调亮度对比度 一条链换成两条 同时出大小两版 输出改成推流地址 直播
四段骨架不动,只换中间——这正是责任链的价值在代码层面的体现。 第十三章的直播推流就是这个骨架加一次替换:把封装的目标从文件换成RTMP 地址,其余全一样。 所以这一节不是练习,是后面几章的地基。
五、第九章收尾
四句话,把这一章的账结掉。 记住的 一句话
滤镜在哪 五环里的「处理」,前后两头不用改 图怎么搭 源、滤镜、汇,连线、配好 简单与复杂 看几进几出,不看名字 最容易错 pts 不换算,时长差几百倍
四句话里最后一句最贵,因为它是唯一「不报错但结果错」的一条。 第十章换到音频那头,重采样要处理的也是「格式与刻度」,只是换了一套名词:采样率、声道布局、 样本格式。坑的形状一模一样。
六、动手 Demo:用 API 走完整条滤镜链,并踩一个真的坑
解码 → 滤镜 → 编码 → 封装,跑完先读时长;再补一行 pts 换算,重跑一遍对照。
import av
import av.filter
from fractions import Fraction
def run(out, fix_pts):
c = av.open("tiny.mp4")
st = c.streams.video[0]
o = av.open(out, "w")
vs = o.add_stream("libx264", rate=25)
vs.width, vs.height, vs.pix_fmt = 80, 60, "yuv420p"
g = av.filter.Graph()
src = g.add_buffer(width=160, height=120, format="yuv420p",
time_base=Fraction(1, 25))
sc = g.add("scale", "80:60")
sink = g.add("buffersink")
src.link_to(sc); sc.link_to(sink)
g.configure()
i = 0
for pk in c.demux(st):
if pk.size == 0:
break
for fr in st.codec_context.decode(pk):
src.push(fr)
fr = sink.pull()
if fix_pts:
fr.pts = i # 按帧号重编
for op in vs.encode(fr):
o.mux(op)
i += 1
o.close()
r = av.open(out)
return r.duration / 1000000.0, i
print("不换算", run("bad.mp4", False))
print("按帧号重编", run("ok.mp4", True))
实跑输出:
--------------------------------------------------------------------
用 API 走完整条滤镜链,并踩一个真的坑
--------------------------------------------------------------------
**① 整条链:解码 → 滤镜 → 编码 → 封装**
项 值
---------------------------------
文件 filtered.mp4
帧数 50
包数 50
体积 10280 字节
读回来的时长 1003.56 秒
**文件出来了,尺寸对了,包数也对——但时长大得离谱。** **1003.56 秒**,而素材本身只有 2.02 秒。
**② 坑在哪**
**滤镜出来的帧,pts 还是源码流的刻度**(1/12800 那种),而编码器是照着 1/25 去理解的。
**同一串数字换了尺子,时间就被放大了几百倍。**
**这就是 8.4 说的「三把尺子」在实战里的样子。** **滤镜这一环不会替你换算 pts,它只管像素。**
**③ 修法:给帧重新编号**
加上一行 `o.pts = i`(按帧序号重新编号),时长变成 **2.00 秒**。
做法 时长
-----------------------------
不换算 pts 1003.56 秒
按帧号重编 2.00 秒
**这一行是整个 9.4 最值钱的东西。** 滤镜链跑通不算完,**时间戳对齐才算完。**
**④ 这一章的收尾**
记住的 一句话
---------------------------------------------------
滤镜在哪 五环里的「处理」,前后两头不用改
图怎么搭 源、滤镜、汇,连线、配好
简单与复杂 看几进几出,不看名字
最容易错 pts 不换算,时长差几百倍
**四句话,就是这一章的账。** 第十章换到音频那头,重采样要处理的也是「格式与刻度」,
只是换了一套名词。
不换算的时长是一千多秒,按帧号重编之后是 2.00 秒。 滤镜不会替你换算 pts,它只管像素——这一行得自己加。
30 秒实验
- 把
rate=25改成rate=50,再跑一遍,看两组的时长各是多少。 - 把
fr.pts = i改成fr.pts = i * 2,看时长变成几秒。 - 在滤镜链尾部加一个
format=yuv420p,看输出体积变不变。
常见误区
- 只验「文件出来了、画面对了」。 时长、pts、音画同步都要一起验。
- 以为滤镜会顺手换算时间戳。 它只管像素,时间戳得自己算。
- 按帧号重编就当万事大吉。 丢帧、变速的场合要按时间算。
自测三问(点开对答案)
1. 为什么加了滤镜之后时长会差几百倍? —— 滤镜出来的帧 pts 还是源码流的刻度,编码器却按自己的 time_base 理解。 2. `o.pts = i` 这一行在做什么? —— 把第 i 帧的显示时刻定成第 i 个单位,配合编码器时基换算成秒。 3. 四段骨架里哪一段换掉就变成「加水印」? —— 滤镜那一段,把 scale 换成 overlay。- ☐ (本节完,第九章已完结,下一章 10.1.1 音频基础)