本文主要说明自托管媒体库(以 Octans 播放链路为例)在「尽量 Direct Play、字幕不触发视频转码」前提下,如何把字幕识别、抽取与缓存放在服务端,把 WebVTT / ASS / PGS 等形态的实际绘制交给客户端;并厘清 full-cache、HLS sidecar delay 与烧录路线的边界,避免把排障问题误当成「再烧一遍就好了」。
媒体库里的字幕问题,表面常是「没字幕」「错位」「方块字」「特效糊了」。根因却经常落在另一套取舍上:要不要为了兼容把字幕像素合成进视频?文本轨和位图轨能否共用同一条渲染管线?HLS 从影片中间起播时,完整 sidecar 字幕的时间线谁负责对齐?下面按产品策略与格式边界整理,而不是罗列某一版内部接口字段。
为什么先拒绝「默认烧录」
烧录(softsub → hardsub)把字幕滤镜叠进视频再编码,客户端只需要播画面即可。兼容性最好,但代价也很清楚:
| 维度 | 服务端烧录 | 客户端渲染(sidecar / overlay) |
|---|---|---|
| 视频交付 | 几乎必走转码,Direct Play 被破坏 | 音视频可继续 copy / 直开 |
| 算力与缓存 | CPU/GPU 持续消耗,产物体积大 | 抽取与缓存成本集中在字幕文件本身 |
| 切换字幕 | 常意味着换一路转码输出 | 可只换字幕资源或重挂 overlay |
| 样式保真 | 由滤镜链路决定,客户端难调 | 由客户端渲染器能力决定 |
| 失败策略 | 「烧不出来就播不了」容易绑死主画面 | 可关字幕、换轨、提示不支持,主视频仍可播 |
对「先保住 Direct Play / HTTPFILE,HLS 只在必要时进转码」的媒体库来说,当前版本更合理的默认是:字幕失败时提示或换轨,而不是自动把整路视频拖进烧录。 烧录可以留作后续可选增强,但不应成为「字幕格式一多就先转码」的主路径。
一句话分层:
服务端:识别轨 → 授权抽取 / 轻量转换 → full-cache / 授权分发 → 给出控制面字段(状态、delay 等)
客户端:按 renderer 类型挂载文本轨、ASS 层或位图层 → 与 video 同一坐标系全屏
不做:默认把字幕像素合成进视频再编码
三类字幕,三条渲染边界
先按「本质」而不是文件扩展名分类,后续选型才不会绕:
| 类型 | 代表格式 | 本质 | 常见客户端路径 |
|---|---|---|---|
| Web 文本字幕 | WebVTT;SRT 可转 WebVTT | 纯文本 timed track | 浏览器原生 <track> / 等价 text track |
| 特效文本字幕 | ASS / SSA | 带样式、动画、定位的脚本文本 | DOM/Canvas 类渲染器(如 ASS.js 一类 MIT 路线) |
| 图片字幕 | PGS / SUP、VobSub | 位图 / 图形 | WASM/Canvas 位图渲染(如 libbitsub 一类) |
WebVTT / SRT:标准底座,不是全家桶
WebVTT 是浏览器原生 timed text 路径,适合对白型字幕。SRT 通常在服务端或离线阶段归一成 WebVTT,再交给原生 track,体积小、集成简单。
它解决不了的问题同样明确:复杂 ASS 特效、PGS 位图、以及依赖特定字体附件的屏幕字。WebVTT 是文本字幕的默认基础能力,不能单独覆盖媒体库里的全部样本。
ASS / SSA:保真与许可证的折中
ASS 在动漫与特效字幕场景极常见。可选路线大致有:
- libass-WASM 系:像素级更接近桌面播放器,但体积、字体包与许可链路更重,不一定适合「MIT 优先、闭源商用边界要简单」的默认主线。
- 轻量 JS 渲染器(如 ASS.js):集成简单、按需加载友好;官方也承认 DOM 路径难以与 VSFilter / libass 像素级一致。适合作为「稳定显示优先」的默认,而不是「复杂特效像素保真」的承诺。
工程上更稳妥的字体策略是:镜像内置多语言 fallback 字体(例如 Noto 系列按 SC/TC/JP/KR 分包)+ 前端用 @font-face alias 映射 ASS 里出现的字体名,优先消灭方块字;不必默认把 MKV 附件里的商业字体整包分发。代价是屏幕字 / OP ED 的「原字体观感」会变——产品上应接受「默认稳定显示,可选尽量保留系统原字体」。
全屏也必须注意:只对 <video> 调原生全屏时,ASS 覆盖层可能不进全屏。播放器应让 video + 字幕 host 处于同一 wrapper,并对 wrapper 请求全屏。
PGS / SUP:位图问题,不是字体问题
PGS 是图形字幕,没有「缺字体出方块」这一类文本问题。典型链路是:服务端 lazy extract 到 .sup(或等价缓存)→ 授权 URL → 客户端位图层按时间轴贴图。
风险集中在对齐与性能:
| 风险 | 说明 |
|---|---|
| 显示区域对齐 | object-fit、letterbox / pillarbox 会让 canvas 与真实画面错位 |
| 分辨率不一致 | 4K 视频 + 1080p PGS 需要正确缩放与 presentation size |
| seek | 大 .sup 需要缓存与 seek 后快速恢复,不能假设「开过一次就永远瞬时」 |
| 设备能力 | WebGPU / WebGL2 / OffscreenCanvas 在电视浏览器上不稳定时,需要 Canvas2D 一类 fallback |
| 产品边界 | 一般不做 OCR、不转 WebVTT、不默认烧进视频 |
漏显 / 晚显时,先确认资源是否 ready,再核对时间线偏移与当前播放位置是否一致——这比先怀疑「解码器坏了」更高效。
服务端职责:识别、抽取、full-cache,而不是画像素
客户端渲染并不等于「服务端什么都不做」。相反,轨事实与资源生命周期仍应集中在服务端,避免每个客户端各自 demux、各自猜语言、各自缓存半截文件。
识别与展示轨
媒体库通常同时存在:
- 容器内嵌字幕轨(有 stream index,可被 FFmpeg map / 抽取)
- 外置 sidecar 字幕(归属图谱 / 路径匹配;可能共享、歧义、缺失)
展示层需要语言、标题、forced / default 等可读信息,但播放映射仍应对齐 demuxer 事实或明确的外置资产 ID。文件名启发式可以补语言尾缀,内容级语言检测、OCR 猜语种则通常不是第一阶段必做项。命名策略(单挂载跟随宿主 stem、共享字幕用规范 stem、保留 forced/sdh 等语义尾缀)影响的是库整理与预览,不是渲染器本身,但会决定用户选轨时看到什么。
抽取与转换(仍不转码视频)
| 输入 | 服务端常见动作 | 客户端动作 | 是否转码视频 |
|---|---|---|---|
| WebVTT | 直接或鉴权后返回 | 原生 text track | 否 |
| SRT | 转 WebVTT 后缓存 | 原生 text track | 否 |
| ASS / SSA | 即时抽取文本,可缓存 raw ASS | ASS 渲染层 + 字体 alias | 否 |
| PGS / SUP | lazy extract 到 .sup 等缓存 | 位图层渲染 | 否 |
| 无法渲染 | 返回失败 / 不支持诊断 | UI 提示、关字幕或换轨 | 否 |
ASS 抽取成本相对低,但仍应缓存,避免每次切轨、刷新都起进程。PGS 产物往往更大,更适合「首次请求再 extract + 持久缓存 + 失效键含文件 size / mtime / stream 索引」一类策略。外置 ASS 还要注意历史编码(UTF-8 / GBK / Big5 / Shift-JIS 等):返回前端前尽量归一到 UTF-8,检测不确定时把诊断暴露给排障,而不是静默乱码。
full-cache:字幕就绪态,不等于视频 segment 范围
播放会话里常见的「字幕 preparing / ready」与 HLS 视频 window 是两条线:
视频 HLS:window 内 segment 按时间窗生成 / 可服务
字幕 full-cache:整轨(或约定形态)是否已可挂载
因此:
- seek 命中视频 cache,不保证字幕 full-cache 已完成。
- full-cache 未就绪时,控制面应快速给出 preparing / 可重试语义,而不是让客户端对 track URL 挂一分钟超时。
- 只切换字幕时,理想情况是逻辑会话与主视频交付方式不变:HTTPFILE 不必重签整路视频;HLS window 未失效时也不必为换字幕重启整路转码。
- 客户端应短轮询字幕-only 状态到 ready,不要用「重建整个播放会话」当默认重试。
字幕资源、抽取缓存与外置路径都可能泄露片源结构与语言偏好,应走授权 URL 或受会话保护的接口;镜像内置 fallback 字体可以是公开静态资源,用户字幕本体则不应裸奔。
HLS 与 sidecar delay:谁对齐时间线
Direct Play / HTTPFILE 下,字幕时间线通常与原片一致,客户端按媒体时间挂载即可(仍要注意浏览器 in-band track 抢显:非业务托管的 textTracks 应保持 disabled)。
HLS 则更绕一层。转码 / DirectStream 的 window 往往不是从影片 0 点开始,而 native 侧常见产品形态又是:
视频:HLS playlist + segment(窗口时间线)
字幕:完整 sidecar 文件(原片时间线)+ 服务端给出的 delay / offset
关键原则:
- delay 是控制面字段,由服务端根据 window 起点等事实给出。
- 客户端应消费返回值,不要在本地用 windowStart 自己重推一套。 若 window 起点非 0 而客户端 delay 仍为 0,字幕会按「从影片开头」显示,表现为整体偏早 / 错位。
- Web/hls.js 路径上可能另有分段 WebVTT 呈现;native/libmpv 路径则更常是完整 sidecar + delay。两条产品路径不要混用同一套「我以为该怎么对齐」的假设。
- PGS 一类位图轨同样依赖 ready 状态与 renderer 偏移;漏显时先对 descriptor / 资源状态,再对时间字段。
和排障指南同一口径:服务端决策,客户端执行。客户端不应本地重算应由会话响应给出的字幕 delay,也不应把前端内部的「偏好对象」整包打给只消费最终 track id 的接口。
前端结构:一层视频,多层字幕 host
概念上播放器 DOM 可以收敛成:
player-root(全屏目标)
├─ video
└─ subtitle-host
├─ text / WebVTT 层
├─ ASS 层
└─ bitmap / canvas 层
实践建议:
- 按 renderer 按需动态 import,避免把 ASS / 位图 WASM 打进首屏主包。
- 切换轨时销毁旧实例,避免双层字幕叠显。
- UI(选轨、字体策略、诊断)与渲染层解耦:控件只改选择与设置,不直接碰解码。
- 复杂 ASS 可接受 best-effort 时,在诊断或说明里写清「不保证像素级一致」,比静默「看起来像坏了」更好。
- HTTPFILE 若浏览器拒绝关闭 in-band 原生字幕轨,该样本不宜当作可靠的浏览器直开字幕路径,应考虑 remux / HLS 等替代交付。
与「识别」专题的交界
渲染关心「怎么画」;识别关心「有哪些轨、叫什么、归属谁」。交界处常见摩擦:
- 外置共享字幕在图谱里一对多,展示层仍宜按「每个媒体文件一行」投影,避免选轨模型变成多对多主键。
- 语言标签影响 ASS fallback 字体选 SC 还是 JP/KR:错选不一定出方块,但会出「字形不对」。优先级可粗排为:轨 language tag → 文本侧线索 → 库默认语言 → 用户手动。
- forced / default / signs 等语义影响自动选轨与 UI 文案,但不改变「文本 vs 位图」渲染器选择。
- 内容 OCR、完整字幕组命名知识库、附件字体完整分发,通常是后置专项,不应阻塞「客户端渲染主线」落地。
推荐样本与验收意识
进入实现或回归前,建议至少准备:
| 样本 | 验证点 |
|---|---|
| 中英 SRT | SRT→WebVTT、UTF-8、原生 track |
| 简单 ASS 对白 | 基础渲染与切轨 |
| 缺字体 CJK ASS | fallback 是否消方块、区域字体是否合理 |
| 复杂动漫 ASS | 特效偏差是否可接受、诊断是否诚实 |
| 1080p / 4K PGS | 缩放、对齐、seek 后恢复 |
| Forced 轨 | 自动选轨与「用户关字幕」语义 |
| Safari / 电视浏览器 | text track、Canvas/WASM fallback、全屏 overlay |
许可上:前端默认主线优先 MIT 友好组件;Noto 等 OFL 字体可随应用分发但需遵守 OFL(不可单独售卖字体等)。具体版本应锁定并自测,而不是把 README 能力表直接写成产品承诺。
注意事项
- 本文是架构与边界说明,不是某一版本 API 契约或字段清单;路径与状态名以目标环境为准。
- 「客户端渲染」不等于「零服务端成本」;抽取、full-cache、鉴权与 delay 仍是服务端硬职责。
- ASS 稳定显示 ≠ 像素级保真;PGS 能贴图 ≠ 全设备 60fps 无压力。
- 字幕 full-cache 与 HLS 视频 segment 范围无关;二者 ready 条件不要混读。
- 默认不做烧录是产品策略,不是物理学定律;若未来引入烧录,应作为显式降级路径,并与 Direct Play 主线分开度量成本。
到这里可以把客户端字幕策略压成三句:用识别与缓存把轨事实和资源生命周期收在服务端;用 WebVTT / ASS / 位图三条渲染边界覆盖常见格式且不拖垮视频 Direct Play;用 full-cache 状态与 HLS sidecar delay 等控制面字段对齐时间线,失败时换轨或提示,而不是默认烧录整路视频。
相关阅读
同系列还可对照:
- 媒体库播放排障:从会话创建到 HLS / DirectPlay / 字幕链路:会话、full-cache 状态、sidecar delay 与 HTTP 面分场景定位。
- Octans Android 播放内核演进:从 libmpv 单内核到双内核实验:原生侧复杂字幕与 DirectPlay 对播放内核选型的影响。
- 媒体库轨识别与动态范围:stream index、语言标题与 HDR/DV 分层探针:内嵌轨与外置 sidecar 的事实分层,以及展示轨与播放映射的边界。
来源
本文综合改写自 Octans 项目研究文档中的客户端字幕渲染与无烧录播放方案,以及字幕识别专项中与轨归属、命名与外置投影相关的边界说明。表述面向公开工程讨论,已去掉内部 DTO / 接口清单与未发布实现细节;具体 renderer 版本、设备矩阵与 API 字段以目标环境复测为准。