字幕处理:SRT/ASS字幕解析、字幕渲染、硬字幕与软字幕嵌入

字幕处理,说白了就是给视频配上文字说明。这件事看起来简单,但坑不少。我最早做播放器的时候,觉得字幕不就是显示几行字嘛,结果被各种编码、格式、时间轴偏移折磨得够呛。今天咱们就把这块彻底讲透。

字幕格式:SRT 与 ASS 的区别

先聊聊最常见的两种字幕格式。SRT 是最简单的,ASS 则复杂得多。

特性 SRT ASS
结构 纯文本 + 时间轴 脚本 + 样式 + 事件
样式支持 基本无(仅颜色) 字体、阴影、边框、动画
解析难度 简单 中等
文件大小 较大

我个人习惯,如果只是做简单的翻译字幕,用 SRT 就够了。但要做特效字幕,比如卡拉OK、滚动弹幕,那就得上 ASS。

SRT 字幕解析

SRT 的格式非常直白:序号、时间轴、文本。我写解析器的时候,核心就是按行读取,用正则匹配时间轴。

// SRT 解析核心逻辑
std::vector<SubtitleEntry> parseSRT(const std::string& content) {
    std::vector<SubtitleEntry> entries;
    std::istringstream stream(content);
    std::string line;
    
    while (std::getline(stream, line)) {
        // 跳过空行
        if (line.empty()) continue;
        
        // 解析序号
        int index = std::stoi(line);
        
        // 解析时间轴
        std::getline(stream, line);
        // 格式: 00:01:23,456 --> 00:01:25,678
        auto [start, end] = parseTimeRange(line);
        
        // 解析文本(可能有多行)
        std::string text;
        while (std::getline(stream, line) && !line.empty()) {
            text += line + "\n";
        }
        
        entries.push_back({index, start, end, text});
    }
    return entries;
}
小技巧:时间轴解析时,注意毫秒分隔符。SRT 用逗号,ASS 用点。我当年没注意这个,结果字幕快了半秒,排查了半天。

ASS 字幕解析

ASS 的解析就复杂多了。它分为几个区:Script Info、V4+ Styles、Events。我建议用现成的库,比如 libass,别自己从头写。

// ASS 样式定义示例
struct ASSStyle {
    std::string name;        // 样式名
    std::string fontName;    // 字体
    int fontSize;            // 字号
    uint32_t primaryColor;   // 主色
    uint32_t secondaryColor; // 辅色
    uint32_t outlineColor;   // 边框色
    uint32_t shadowColor;    // 阴影色
    bool bold;               // 粗体
    bool italic;             // 斜体
    int borderStyle;         // 边框样式
    int outlineWidth;        // 边框宽度
    int shadowDepth;         // 阴影深度
};

ASS 的事件行格式是:Dialogue: 0,0:01:00.00,0:01:05.00,Default,,0,0,0,,Hello World。中间那些逗号分隔的字段,每个都有含义。我曾经在解析时漏了一个字段,结果字幕全乱了。

字幕渲染:从文本到像素

渲染字幕,本质上就是把文字画到画面上。这里有几个关键点:

  • 字体加载:系统字体 vs 内嵌字体。ASS 字幕经常指定字体,找不到就回退。
  • 抗锯齿:字幕边缘要平滑,不然看着像锯齿。
  • 编码处理:UTF-8 是主流,但老字幕可能是 GBK、Shift-JIS。

我推荐用 FreeType 做字体渲染,配合 OpenGL 或 Direct2D 做合成。libass 内部就是这么干的。

// 使用 libass 渲染字幕
ASS_Library* ass_lib = ass_library_init();
ASS_Renderer* ass_renderer = ass_renderer_init(ass_lib);

// 加载字幕文件
ass_process_file(ass_renderer, "subtitle.ass");

// 渲染某一帧
ASS_Image* img = ass_render_frame(ass_renderer, 
                                   video_width, video_height,
                                   frame_timestamp);
// 遍历图像链表,合成到视频帧
for (ASS_Image* p = img; p; p = p->next) {
    // 将字幕图像混合到视频帧
    blend_subtitle_to_frame(frame, p);
}
注意:ASS 的动画效果(如滚动、淡入淡出)需要逐帧渲染。如果性能不够,可以考虑缓存静态字幕帧。

硬字幕与软字幕

这两个概念,我当年也搞混过。简单说:

  • 硬字幕:直接烧录到视频画面里,无法移除。
  • 软字幕:作为独立流或文件,播放时叠加。

硬字幕适合发布到平台(如 B站、YouTube),因为平台会转码,软字幕可能丢失。软字幕则适合本地播放,用户可以自由开关。

硬字幕嵌入实现

硬字幕嵌入,说白了就是把字幕渲染到每一帧上,然后重新编码。我用 FFmpeg 的滤镜来做:

// FFmpeg 硬字幕嵌入命令
// 使用 subtitles 滤镜
ffmpeg -i input.mp4 -vf "subtitles=subtitle.srt" output.mp4

// 或者用 libass 滤镜(支持 ASS 特效)
ffmpeg -i input.mp4 -vf "ass=subtitle.ass" output.mp4

代码层面,就是逐帧解码、渲染字幕、编码输出。性能瓶颈在编码,不是字幕渲染。

软字幕嵌入实现

软字幕是把字幕作为独立流封装到容器里。MP4 用 mov_text,MKV 用 SRT/ASS 原生流。

// FFmpeg 软字幕嵌入
ffmpeg -i input.mp4 -i subtitle.srt \
       -c copy -c:s mov_text \
       output.mp4

// 或者封装到 MKV
ffmpeg -i input.mp4 -i subtitle.ass \
       -c copy -c:s ass \
       output.mkv
经验之谈:软字幕的编码格式要和容器匹配。MP4 的 mov_text 只支持基本样式,ASS 特效会丢失。我建议用 MKV 封装 ASS 字幕。

知识体系总览

下面这张图,是我整理的字幕处理全流程。从解析到渲染,再到嵌入,每一步都有坑。

字幕处理知识体系 SRT 字幕文件 ASS 字幕文件 内嵌字幕流 字幕解析器(时间轴、样式、事件) 字幕渲染(FreeType + libass + OpenGL) 硬字幕(烧录到视频帧) 软字幕(独立流封装) 图:字幕处理从源文件到最终嵌入的完整流程

避坑指南

做字幕处理这些年,我踩过的坑不少。挑几个典型的说说:

  • 编码问题:我曾经遇到一个 SRT 文件,用 UTF-8 打开全是乱码。后来发现是 GBK 编码。建议统一用 UTF-8,解析前做编码检测。
  • 时间轴偏移:ASS 字幕的时间轴是毫秒级精度,但有些播放器会四舍五入。我建议在解析时保留浮点数,不要转整数。
  • 字体缺失:ASS 字幕指定了某个字体,但系统没有。这时候要回退到默认字体,或者从字幕文件里提取内嵌字体。
  • 性能问题:ASS 的逐帧动画很吃 CPU。如果视频是 60fps,字幕渲染跟不上,就会掉帧。我一般会缓存静态帧,只对动态部分重新渲染。
核心总结:字幕处理的关键在于格式解析的健壮性、渲染的性能、以及嵌入方式的正确选择。硬字幕适合发布,软字幕适合本地。ASS 特效虽好,但兼容性不如 SRT。

嗯,字幕这块就聊到这儿。下次你遇到字幕显示不对,先检查编码,再看时间轴,最后看字体。这三步走完,90% 的问题都能解决。

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