硬字幕用 subtitles 滤镜,靠 libass;软字幕只加一条流,视频音频都不重编。 这台机器没 libass,所以改用 Pillow 画字 + overlay,效果等同硬字幕。
上一节讲清了硬软两种字幕,这一节给两条能用的命令。 它也是第七章最后一篇,把 overlay、movie、loop 这几件老工具又用了一遍。
一、硬字幕的标准命令
一条命令就烧进去:
ffmpeg -i s7.mp4 -vf subtitles=x.srt -c:a copy out.mp4
subtitles 这个滤镜靠 libass 干活,字幕样式、换行、位置都由它算。
ASS 文件也用它——subtitles=x.ass 即可,不需要换成 ass 滤镜。
-c:a copy 让音频不重编码,能省一大半时间。
视频这边必须重编,因为画面真的变了。
二、沙箱里没有 libass
老实说:这台机器上的 PyAV 没编进 libass,也没有 drawtext, 所以「烧字幕」这条路在这里走不通:
滤镜 在这台机器上
subtitles 没有 ass 没有 drawtext 没有
命令行版 ffmpeg 一般默认带 libass,所以「同一个滤镜在你机器上有、在我这儿没有」 是很常见的事——滤镜不是 ffmpeg 内置的,是一堆外部库编进来的。
遇到「no filter xxx」这种错,先别怀疑写法,先查滤镜在不在。
ffmpeg -filters 一式就清楚,比猜快得多。
三、用 Pillow 画字再贴上去
既然文字不能由 ffmpeg 画,就换个人画:
拿 Pillow 把字幕画成一张半透明图片,再用 movie 读图、loop 循环、overlay 贴上去。
效果跟烧字幕一样——文字变成了画面的一部分。
这也是「硬字幕」的本质:不是必须有 subtitles 滤镜,
而是「文字最终落在像素里」。谁把字画成图都行。
movie 读的是一张 png,它只有一帧,所以必须接 loop=loop=-1:size=1
把它撑成无限长的源——-1 表示无限循环,size=1 表示缓冲一帧。
这套写法的实用价值不止于「绕过缺滤镜」: 做角标、做水印、做片头 Logo,都是同一个套路——画一张图,贴上去。
四、贴上去之后确实变了
量一下字幕带那一条的亮度,就能确认它真的进了画面:
区域 均值
原画面第 70~88 行 157.1 贴了字幕带之后 65.5
从 157.1 掉到 65.5,说明那一块被半透明的黑带压暗了—— 字幕确实烧进了画面,不是「叠在上面可以关掉」的软字幕。
软字幕的写法完全不一样,只加流不动画面:
ffmpeg -i s7.mp4 -i x.srt -c:v copy -c:a copy -c:s mov_text out.mp4
-c:v copy -c:a copy 表示视频音频都不重编码,只把字幕流加进去。
这一步几乎瞬间完成,因为一个像素都没重算。
两条命令一比就看出来了:软字幕是「加一条流」,硬字幕是「重编一遍画面」。 所以能软就别硬,硬是最后一步的事。
五、动手 Demo:画一条字幕带、贴上去、量亮度
下面这段代码用 Pillow 画一条半透明黑带加白字, 再用 movie + loop + overlay 贴到画面上,最后量那一块的均值。
import av
from PIL import Image, ImageDraw, ImageFont
c0 = av.open(u"s7.mp4")
BASE = next(c0.decode(video=0)).to_ndarray(format=u"yuv420p")
c0.close()
im = Image.new(u"RGBA", (160, 18), (0, 0, 0, 160))
d = ImageDraw.Draw(im)
d.text((6, 3), u"subtitle 001", fill=(255, 255, 255),
font=ImageFont.load_default())
im.save(u"band.png")
print(u"--- 字幕带画好了:160x18,黑色 + alpha 160")
g = av.filter.Graph()
b = g.add_buffer(width=160, height=90, format=u"yuv420p",
time_base=__import__(u"fractions").Fraction(1, 25))
mv = g.add(u"movie", u"band.png")
lp = g.add(u"loop", u"loop=-1:size=1")
ov = g.add(u"overlay", u"0:70")
sk = g.add(u"buffersink")
b.link_to(ov, 0, 0)
mv.link_to(lp)
lp.link_to(ov, 0, 1)
ov.link_to(sk)
g.configure()
vf = av.VideoFrame.from_ndarray(BASE, format=u"yuv420p")
vf.pts = 0
vf.time_base = __import__(u"fractions").Fraction(1, 25)
b.push(vf)
o = sk.pull().to_ndarray(format=u"rgb24")
orig = av.VideoFrame.from_ndarray(BASE, format=u"yuv420p")
orig = orig.to_ndarray(format=u"rgb24")
print(u"--- 第 70~88 行均值")
print(u" 原画面 %.1f" % orig[70:88].mean())
print(u" 贴字幕带后 %.1f" % o[70:88].mean())
实跑输出:
--------------------------------------------------------------------
7.17.2 字幕处理
--------------------------------------------------------------------
**① 硬字幕的标准命令**
**一条命令就烧进去:**
`ffmpeg -i s7.mp4 -vf subtitles=x.srt -c:a copy out.mp4`
**`subtitles` 这个滤镜靠 libass 干活,字幕样式、换行、位置都由它算。**
**ASS 文件也用它——`subtitles=x.ass` 即可,不需要换成 `ass` 滤镜。**
**② 沙箱里没有 libass,这一步跑不了**
**老实说:这台机器上的 PyAV 没编进 libass,也没有 drawtext,**
**所以「烧字幕」这条路在这里走不通:**
滤镜 在这台机器上
------------------------------
subtitles 没有
ass 没有
drawtext 没有
**命令行版 ffmpeg 一般默认带 libass,所以「同一个滤镜在你机器上有、在我这儿没有」**
**是很常见的事——滤镜不是 ffmpeg 内置的,是一堆外部库编进来的。**
**③ 用 Pillow 画字,再 overlay 贴上去,等效于硬字幕**
**既然文字不能由 ffmpeg 画,就换个人画:拿 Pillow 把字幕画成一张半透明图片,**
**再用 `movie` 读图、`loop` 循环、`overlay` 贴到画面上。**
**效果跟烧字幕一样——文字变成了画面的一部分。**
**这也是「硬字幕」的本质:不是必须有 `subtitles` 滤镜,**
**而是「文字最终落在像素里」。谁把字画成图都行。**
**④ 贴上去之后,那一块确实变了**
区域 均值
-------------------------------
原画面第 70~88 行 157.1
贴了字幕带之后 65.5
**从 157.1 掉到 65.5,说明那一块被半透明的黑带压暗了——字幕确实烧进了画面,**
**不是「叠在上面可以关掉」的软字幕。**
**⑤ 软字幕的标准命令**
`ffmpeg -i s7.mp4 -i x.srt -c:v copy -c:a copy -c:s mov_text out.mp4`
**`-c:v copy -c:a copy` 表示视频音频都不重编码,只把字幕流加进去。**
**这一步几乎瞬间完成,因为一个像素都没重算。**
**两条命令一比就看出来了:软字幕是「加一条流」,硬字幕是「重编一遍画面」。**
**所以能软就别硬,硬是最后一步的事。**
字幕带是 160×18、黑色、alpha 160——半透明,这样底下的画面还能透一点出来。 alpha 写满 255 就成了纯黑条,看着像贴了块胶布。
贴的位置是 0:70,也就是画面底部往上一点。
160×90 的画面里,第 70 行往下还剩 20 行,正好放一条 18 行高的字幕。
最后两行是本节的关键证据:157.1 掉到 65.5。 数字变了,说明文字真的进了像素,而不是「叠在上面能关掉」的软字幕。
代码里 __import__(u"fractions").Fraction(1, 25) 是为了少写一行 import,
实际写脚本时老老实实 from fractions import Fraction 更清楚。
30 秒实验
- 改 alpha:把 160 改成 80,看那一块是不是只掉一点点。
- 改位置:把
0:70改成0:36,字幕跑到画面中间。 - 改文字:多画两行字,看块高够不够——不够会被裁掉下半截。
常见误区
- 以为必须有
subtitles滤镜才能烧字幕。文字落在像素里就是硬字幕。 - 把软字幕当硬字幕。
-c:s mov_text只加流,播放器一关就没了。 - 忘了给 png 加
loop。一张图只出一帧,贴完就停了。
自测三问(点开对答案)
1. 硬字幕的标准命令用哪个滤镜? —— `subtitles`,它依赖 libass。 2. 这台机器为什么烧不了字幕? —— PyAV 没编进 libass,`subtitles`、`ass`、`drawtext` 三个滤镜都没有。 3. 软字幕命令里 `-c:v copy` 是什么意思? —— 视频流不重编码,原样复制,只加一条字幕流。- ☐ 7.17 完,第七章 47 篇全部读完,下一章 8.1.1 RTSP 简介