字幕分两类:文本的能转格式、能改样式,图形的一个字都改不了。 国内素材最常踩的坑是字符编码,而且是静默失败。
3.5 和 3.6 讲音视频的编码参数,这一节讲字幕这个特殊的一路。 3.8 讲改变处理范围的选项。
一、字幕分成截然不同的两类
这个分类决定了你能对字幕做什么操作:
类别 本质 代表格式
文本字幕 文字+时间轴 srt/ass/webvtt 图形字幕 一张张图片+时间轴 dvdsub/pgssub
文本字幕能转格式、能改字体、能翻译;图形字幕只能原样搬。 因为它的内容是「像素」,不是一个字——改一个字就等于重做一张图。
所以处理图形字幕只有两条路:原样 copy,或者烧进画面(变成硬字幕)。 两条路都不能改内容,只能整体的搬或者整体的烧。
二、四个相关选项
字幕选项很少,每一个的用途都很明确:
选项 作用 用错会怎样
-c:s 字幕编码器 选错报 could not find tag -sn 不要字幕 — -map 0:s:0 只取第一条字幕 漏了字幕一条都进不去 -sub_charenc 源文件的字符编码 乱码,且静默
第四行是国内素材最常踩的坑:老的 srt 常是 GBK 编码。
不写 -sub_charenc gbk 就会把中文读成乱码,而且它不报错——
你以为成功了,打开一看全是问号。
判断方法很简单:转完先 head -5 看一眼输出字幕。
十秒能确认的事,比事后重新转一遍便宜。
三、内嵌与外挂
两种做法各有代价,取决于你后面还要不要改字幕:
做法 命令要点 优缺点
内嵌 -c:s mov_text(mp4)或 copy(mkv) 一体,但改不了 外挂 不写 -map 0:s,单独留 srt 能改,但要带两个文件
第一行有个容器限制:mp4 只支持 mov_text 这一种文本字幕。 ass 的特效字幕塞不进 mp4,只能烧进画面——那就变成硬字幕了。
硬字幕一旦烧进去就再也关不掉、换不了语言。 所以「要不要烧」这件事必须一次决定,它是所有字幕处理里最不可逆的一步。
四、动手 Demo:六种格式,两条路
手写一个三段式的 SRT 文件,用库把它当容器读回来。 目的是确认「SRT 本身就是一个容器格式」,以及看清其中的包结构。
import os
import av
# 手写 SRT:三段,每段是 序号/时间轴/文字
txt = (u"1\n00:00:00,000 --> 00:00:00,700\n第一段字幕\n\n"
u"2\n00:00:00,700 --> 00:00:01,400\n第二段字幕\n\n"
u"3\n00:00:01,400 --> 00:00:02,000\n第三段字幕\n\n")
with open(u"sub.srt", u"w", encoding=u"utf-8") as f:
f.write(txt)
print(u"SRT 文件 %d 字节" % os.path.getsize(u"sub.srt"))
# 当容器读
c = av.open(u"sub.srt")
print(u"容器 %s 流 %d 条 类型 %s"
% (c.format.name, len(c.streams), c.streams[0].type))
n = 0
first = None
for pk in c.demux(subtitles=0):
if not pk.size:
continue
n += 1
if first is None:
first = pk.pts
c.close()
print(u"包 %d 个,首段 pts %s" % (n, first))
实跑输出:
--------------------------------------------------------------------
3.7 字幕选项——六种格式,两条路
--------------------------------------------------------------------
**① 字幕分成截然不同的两类**
类别 本质 代表格式
----------------------------------------------------------
文本字幕 文字+时间轴 srt / ass / webvtt
图形字幕 一张张图片+时间轴 dvdsub / pgssub
**这两类的处理方式完全不同:文本字幕能转格式、能改字体;**
**图形字幕只能原样搬,改一个字都要重新做图。**
**② 相关的选项只有四个**
选项 作用 用错会怎样
-------------------------------------------------------------------------
-c:s 字幕编码器 选错就报 could not find tag
-sn 不要字幕 —
-map 0:s:0 只取第一条字幕 漏了就一条都没进输出
-sub_charenc 字幕源文件的字符编码 乱码,尤其 GBK 素材
**最后一行是国内素材最常踩的坑:老的 srt 常是 GBK 编码,**
**不写 `-sub_charenc gbk` 就会把中文读成乱码,而且是静默的——不报错。**
**③ 手写一个 SRT,读回来看看里面到底是什么**
项 值
---------------------------------------
文件字节 159
流条数 1(类型 subtitle)
包数(段数) 3
首段 pts 0
**一个 SRT 就是一个「零散的文本文件」,ffmpeg 把它当容器读,每一段是一条包。**
**这就是为什么 srt 既能当输入也能当输出——它本身就是一个容器格式。**
**④ 三种文本字幕格式的体积对比**
格式 特点 体积趋势
---------------------------------------------------------
srt 最简,只有序号+时间+文字 最小
ass 带样式、定位、特效 大几倍
webvtt 网页用,语法接近 srt 和 srt 接近
**ass 大出来的部分几乎全是样式表——字体、颜色、位置、动画。**
**所以「只想要字幕内容」的话,srt 转 ass 是纯浪费;**
**反过来,要控制字幕位置和字体,srt 根本表达不了,必须用 ass。**
**⑤ 内嵌字幕 vs 外挂字幕**
做法 命令要点 优缺点
------------------------------------------------------------------------
内嵌 -c:s mov_text(mp4)或 copy(mkv) 一体,但改不了
外挂 不写 -map 0:s,单独留 srt 能改,但要带两个文件
**mp4 里的字幕只支持 mov_text,这是容器限制;mkv 什么都装得下。**
**所以要把 ass 特效字幕塞进 mp4,只能先烧进画面——那就变成「硬字幕」了,**
**再也没法关掉,也没法换语言。**
一个 SRT 就是一个纯文本文件,可 ffmpeg 把它当容器读—— 所以 srt 既能当输入也能当输出,它本身就是一个容器格式。
三行文字被切成了三个包,每个包对应一个时间区间。 这个结构跟视频的包一模一样,只是内容从「压缩的画面」变成了「一行字」。
首段 pts 是 0,因为第一段从 0 秒开始。 如果第一段从第 3 秒开始,pts 就不会是 0——时间戳反映的是内容位置,不是文件顺序。
注意 demux 用的是 subtitles=0 而不是 subtitle=0。
PyAV 的复数形式是个容易踩的小坑,写错了直接抛 AttributeError。
30 秒实验
- 转格式:
ffmpeg -i 字幕.srt 字幕.ass,比两边的体积与内容。 - 解编码:
ffmpeg -sub_charenc gbk -i 老字幕.srt 出.srt,中文不再乱码。 - 内嵌:
ffmpeg -i 片子.mp4 -i 字幕.srt -c:s mov_text -map 0 -map 1 出.mp4,注意两次-map。
常见误区
- 忘写
-sub_charenc gbk。GBK 的 srt 会被读成乱码,而且不报错,静默失败。 - 把 ass 塞进 mp4。mp4 只支持 mov_text,ass 的特效塞不进去,只能烧画面。
- 以为硬字幕还能改。烧进画面就变成像素了,关不掉也换不了语言,是不可逆的一步。
自测三问(点开对答案)
1. 为什么 SRT 既能当输入也能当输出? —— 因为它本身就是一个容器格式。纯文本、结构简单,但每一段有独立的时间轴,ffmpeg 能当容器读写。 2. 图形字幕为什么改不了一个字? —— 因为它的内容是像素图片,不是一个字。改字等于重做一张图,只能整体替换或整体烧进画面。 3. GBK 编码的 srt 转完是乱码,怎么修? —— 加 `-sub_charenc gbk` 告诉 ffmpeg 源文件的编码。它默认按 UTF-8 读,读错也不报错。- ☐ (本节完,下一节 3.8 高级选项)