14、提取字幕轨道:处理文本字幕(SRT)与图形字幕
字幕提取,说实话,是音视频开发里容易被忽视的一块。
很多人觉得字幕嘛,不就是文本吗?直接读出来不就行了?
嗯,真上手做一次你就知道了——坑不少。尤其是图形字幕,处理起来跟文本字幕完全是两码事。
今天我就带你把这部分彻底捋清楚。
14.1 字幕轨道的两种形态
MediaExtractor 拿到字幕轨道后,你首先要判断:这是文本字幕还是图形字幕?
判断依据很简单——看 MIME 类型。
| 类型 | MIME 示例 | 说明 |
|---|---|---|
| 文本字幕 | application/x-subrip | SRT 格式,纯文本,体积小 |
| 图形字幕 | application/x-pgs | Blu-ray PGS 格式,位图,体积大 |
我个人习惯是:先读 MIME,再决定走哪条路。别上来就当成文本处理,否则遇到 PGS 字幕你会一脸懵。
14.2 文本字幕(SRT)提取实战
SRT 字幕的结构其实很简单:
1
00:00:01,000 --> 00:00:04,000
Hello, this is a subtitle.
2
00:00:05,000 --> 00:00:08,000
This is the second subtitle.
每一段由序号、时间戳、文本组成。MediaExtractor 提取出来的数据,就是这些文本的字节流。
核心代码如下:
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(videoPath);
int trackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime != null && mime.equals(MediaFormat.MIMETYPE_TEXT_SUBRIP)) {
trackIndex = i;
break;
}
}
if (trackIndex >= 0) {
extractor.selectTrack(trackIndex);
ByteBuffer buffer = ByteBuffer.allocate(1024 * 10);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.size = sampleSize;
info.presentationTimeUs = extractor.getSampleTime();
// 这里拿到的是 SRT 文本的字节数据
byte[] data = new byte[sampleSize];
buffer.position(0);
buffer.get(data);
String srtContent = new String(data, StandardCharsets.UTF_8);
// 解析 SRT 内容
parseSrtBlock(srtContent, info.presentationTimeUs);
extractor.advance();
}
}
这里有个细节:SRT 的时间戳是 时:分:秒,毫秒 格式,而 MediaExtractor 返回的是微秒。你需要做一次转换。
时间戳转换公式:
SRT 时间(毫秒)= presentationTimeUs / 1000
反过来:presentationTimeUs = SRT 毫秒 * 1000
14.3 图形字幕(PGS)提取实战
PGS 字幕就麻烦多了。
它本质上是一张张位图。每一帧字幕都是一幅图像,包含像素数据、调色板信息、显示区域等。
我在项目中遇到过这种情况:客户给了一个蓝光原盘,字幕轨道是 PGS 格式。一开始我按文本方式读,结果全是乱码。后来才意识到——这根本不是文本,是图片。
PGS 的数据结构大致如下:
PGS 段结构:
- 段类型(1 字节):0x14 表示显示段,0x15 表示调色板段,0x16 表示对象段
- 段长度(2 字节)
- 段数据(N 字节)
提取 PGS 字幕的代码:
if (mime.equals(MediaFormat.MIMETYPE_IMAGE_PGS)) {
extractor.selectTrack(trackIndex);
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB 缓冲区
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.size = sampleSize;
info.presentationTimeUs = extractor.getSampleTime();
byte[] pgsData = new byte[sampleSize];
buffer.position(0);
buffer.get(pgsData);
// 解析 PGS 段
parsePgsSegment(pgsData, info.presentationTimeUs);
extractor.advance();
}
}
解析 PGS 时,你需要自己实现段解析器。说白了就是:
- 先读段类型,判断是调色板还是图像数据
- 如果是调色板段,更新当前调色板
- 如果是对象段,把像素数据 + 调色板合成一张位图
- 记录显示时间,用于同步
注意:PGS 字幕的显示时间不是连续的。它有一个「显示开始」和「显示结束」的概念。你需要维护一个字幕显示列表,根据当前播放时间决定显示哪一帧。
14.4 两种字幕的处理对比
| 维度 | SRT 文本字幕 | PGS 图形字幕 |
|---|---|---|
| 数据量 | 小(KB 级别) | 大(MB 级别) |
| 解析难度 | 低(直接读文本) | 高(需要解析段结构) |
| 渲染方式 | TextView 或 Canvas 绘制 | Bitmap 解码后显示 |
| 内存占用 | 几乎可以忽略 | 每帧字幕都是一张图片 |
| 可编辑性 | 可以修改文本 | 不能直接修改文本 |
你想想看,如果只是做播放器,SRT 字幕用 TextView 就能搞定。但 PGS 字幕你得用 ImageView 或者 SurfaceView 来渲染,而且还得考虑内存回收——我曾经因为没及时回收 Bitmap,导致播放器在播放蓝光原盘时 OOM 了。
14.3 避坑指南
这里我总结几个实际开发中容易踩的坑:
- 编码问题:SRT 字幕可能是 UTF-8、UTF-16 甚至 GBK。不要硬编码 UTF-8,最好先检测 BOM 头。
- 时间戳溢出:有些视频的字幕时间戳会超过 2^31,用 int 会溢出。记得用 long。
- PGS 调色板更新:同一段字幕可能多次更新调色板。不要只缓存最后一张调色板,要按时间顺序应用。
- 字幕重叠:有些视频会有多条字幕同时显示。你的渲染逻辑要支持叠加。
我的建议:如果项目时间紧,优先支持 SRT 字幕。PGS 字幕的解析工作量至少是 SRT 的 5 倍。除非你确实需要蓝光原盘支持,否则别轻易碰 PGS。
14.4 本章小结
字幕提取这件事,说难不难,说简单也不简单。
SRT 字幕就是文本解析,注意编码和时间戳就行。PGS 字幕则是图像解码,需要你理解段结构、调色板、显示时间这些概念。
我个人建议:先搞定 SRT,再考虑 PGS。毕竟大部分在线视频用的都是文本字幕。图形字幕更多出现在蓝光原盘和部分高清流媒体中。
嗯,字幕提取就讲到这里。记住:先判断类型,再选择解析方式。SRT 走文本路线,PGS 走图像路线。两条路都能走通,但别走反了。
公众号:蓝海资料掘金营,微信deep3321