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。毕竟大部分在线视频用的都是文本字幕。图形字幕更多出现在蓝光原盘和部分高清流媒体中。

字幕提取流程 MediaExtractor 读取 MIME 类型 判断字幕类型 application/x-subrip 文本字幕(SRT) application/x-pgs 图形字幕(PGS) 读取字节数据 按 SRT 格式解析 提取文本 + 时间戳 渲染到 TextView 解析段结构 调色板 + 对象段 合成位图 渲染到 ImageView 字幕显示完成

嗯,字幕提取就讲到这里。记住:先判断类型,再选择解析方式。SRT 走文本路线,PGS 走图像路线。两条路都能走通,但别走反了。


公众号:蓝海资料掘金营,微信deep3321