加滤镜不是给转码链「加一环」,而是把中间那一环换掉。 前后两头——解封装与封装——一个字都不用改。
第八章讲的是「把包解成帧、把帧压成包」,那时帧出来就直接进编码器。 这一章讲的是:帧出来先别进编码器,先过一道滤镜。
一、五环里的「处理」
转码这条链一直是五环,滤镜只占其中一环。 环 干什么 有没有滤镜
解封装 把文件拆成压缩包 没有 解码 把压缩包还原成像素帧 没有 处理 改像素、改尺寸、改格式 就是滤镜 编码 把像素帧重新压回去 没有 封装 把压缩包装进容器 没有
所以「加滤镜」的正确说法是「把处理那一环换成滤镜图」。 没加滤镜时这一环不是空的,它只是什么都不做——帧从解码器出来,直接交给编码器。 加上滤镜之后,帧要先从滤镜图的一头进去、另一头出来。
二、加滤镜前后,同一段素材的账
同一段素材,各编三十来个包,量三样东西。 做法 包数 总字节 耗时
不加滤镜 31 6956 二十几毫秒 加 scale 缩放 31 6053 二十几毫秒
包数一样,字节少了一成多。 因为输出尺寸从 160×120 降到 80×60,像素数只有原来的四分之一, 压出来自然小。 耗时几乎一样,两者的差值连跑两遍的抖动都能造出来。 这一条很值得记住:小尺寸视频上缩放本身几乎不花钱,贵的是编解码——解码一帧动辄几毫秒, 缩放一帧只要几十微秒。 所以指望「加个滤镜让程序变快」基本不成立; 反过来说,加滤镜的代价也没那么可怕,真正压性能的仍是编解码与 IO。
三、顺序不能反
三件事有固定的先后,错一步就做不成。 顺序 结果
先解码、再滤镜、再编码 正常 先滤镜、再解码 做不了,压缩数据没法改像素
这条听着像废话,但实际写代码很容易错位:把 push 放到 encode 之后,代码照样跑,也不报错,
只是滤镜永远收不到帧。
这类「不报错的错」最难查。
查的时候不用猜,直接在 pull 之后打一行帧数——是 0 就说明上面那一步接错了。
四、滤镜能改什么
能改的东西分四类,四类的开销差很远。 改什么 代表滤镜 开销
尺寸 scale 小,逐点重采样 像素格式 format 小,换个存法 画面位置 hflip/vflip/crop 极小,只搬不换 内容合成 overlay/drawbox 中,要算混合
前两类会改变「每帧多大」,后两类不改变尺寸。这个区别决定了编码器那边的参数要不要跟着改。 改了尺寸就必须重建编码器:编码器打开时就定死了宽高,中途变尺寸只能关掉重来。 这是直播里改分辨率要「断一下」的根本原因。
五、C 里加滤镜要动的三处
C 代码里,滤镜不是「加一段」,是「改三处」。 位置 原来 加滤镜之后
解码之后 帧直接给编码器 帧先 push 进图 编码之前 从解码器取帧 从图的 sink 取帧 时间戳 沿用解码出来的 必须自己重编
第三处是最容易漏的,也是本章最后那个 Demo 要踩的坑。 滤镜只管像素,不管时间戳——从图里出来的帧,pts 还是原来那套刻度。 同一串数字换了尺子,时间就被放大几百倍。 9.4.2 会实测出这个数。
六、动手 Demo:加滤镜前后,一条转码链差在哪
同一段素材跑两遍:一遍按老样子解码后直接编码,一遍中间插一个缩放滤镜。
import av
import av.filter
from fractions import Fraction
def run(use_filter):
c = av.open("tiny.mp4")
st = c.streams.video[0]
o = av.open("out.mp4", "w")
vs = o.add_stream("libx264", rate=25)
vs.width, vs.height, vs.pix_fmt = 80, 60, "yuv420p"
g = src = sink = None
if use_filter:
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()
n = total = 0
for pk in c.demux(st):
if pk.size == 0:
break
for fr in st.codec_context.decode(pk):
if use_filter:
src.push(fr) # 先滤镜
fr = sink.pull() # 再取回
for op in vs.encode(fr): # 最后才编码
o.mux(op)
n += 1
total += len(bytes(op))
o.close()
return n, total
print(run(False))
print(run(True))
实跑输出:
--------------------------------------------------------------------
加滤镜前后,一条转码链差在哪
--------------------------------------------------------------------
**① 滤镜在五环里的位置**
环 有没有滤镜
---------------------------
解封装 没有
解码 没有
处理 **就是滤镜**
编码 没有
封装 没有
**所以「加滤镜」不是给转码链加一步,而是把中间那一步换成滤镜图。** 前后两头的代码一个字都不用改。
**② 同一段素材,各编二十几个包**
做法 包数 总字节 耗时
---------------------------------------------------
不加滤镜 31 6956 46.6 毫秒
加 scale 缩放 31 6053 25.0 毫秒
**包数一样,字节少了约 13%。** 因为输出尺寸从 160×120 降到 80×60,像素只有原来的四分之一。
**耗时几乎一样,两者只差 21.6 毫秒。** 这点差值连跑两遍的抖动都能造出来,
说明**小尺寸视频上,缩放本身并不贵,贵的是编解码。**
**③ 顺序不能反**
顺序 结果
---------------------------------------------------------
先解码、再滤镜、再编码 正常
先滤镜、再解码 做不了,压缩数据没法改像素
**这一条听着废话,但实际写代码时很容易把 `g.push` 放到 `encode` 之后。**
位置错了不会报错,只会滤镜永远收不到帧。
包数一样、字节少了,因为输出从 160×120 降到 80×60。
src.push 必须在 encode 之前,写在后面不报错,只是滤镜收不到帧。
30 秒实验
- 把
scale换成hflip,看字节数变不变(提示:尺寸没变,字节几乎一样)。 - 把
src.push(fr)挪到vs.encode(fr)之后跑一遍,看输出是不是空文件。 - 把输出高度改成 120(只缩宽不缩高),看编码器会不会报错。
常见误区
- 以为加滤镜是「多加一环」。 它是把「处理」那一环换掉。
- 以为加滤镜一定变慢。 小图上缩放的耗时可忽略,慢的是编解码。
- 滤镜之后再改尺寸却不重建编码器。 编码器的宽高打开时就定死了。
自测三问(点开对答案)
1. 加滤镜之后,转码链的哪一环变了? —— 只有「处理」那一环,解封装与封装完全不变。 2. 为什么加了 scale 之后字节少了、耗时却几乎不变? —— 输出尺寸变小所以字节少;缩放本身很便宜,耗时主要花在编解码上。 3. 滤镜处理完之后,帧上的 pts 还是原来那套吗? —— 是。滤镜只管像素,时间戳要自己按编码器的刻度重算。- ☐ (本节完,下一节 9.1.2 FFmpeg责任链模式简介)