作者| 阿里文娱无线开发专家 黄陂
优酷外挂字幕的目标是实现对全平台和全机型的覆盖,同时能支持 ass 之外的多种字幕格 式、支持字幕互动等多种创新玩法。本文将分享我们对外挂字幕的探索与实践。
一、字幕的简单科普
在开始之前,先科普一些字幕知识。一般而言,字幕分为硬字幕、软字幕和外挂字幕。
硬字幕,或者叫内嵌字幕是视频生产时就直接叠加到视频画面上的,字幕不可取消、不可编辑更改,不支持多语言及国际化。软字幕是将字幕与视频打包在一起,形成独立于音频和视 频 track 流的单独一路 subtitle track 流,整体打包在视频文件中。
我们今天介绍的外挂字幕,是将字幕做成一个独立文件,这些字幕文件可以有多种格式, 独立下发及展示。优点是不破坏视频画面,可随时根据需要更换字幕语言,并且可随时编辑字 幕内容并重新生产发布;缺点是由于字幕文件单独存在,可能会有字幕缺失、字幕跟音视频不 同步等问题;另外,为了确保端上播放必须有字幕,整个链路需要有复杂的硬字幕兜底处理逻辑。
二、优酷视频的字幕现状
1. 优酷字幕链路
目前优酷使用的外挂字幕采用的是 ass 格式,优酷字幕生产链路大致如下:
1)单独生产一路字幕 ass 流,并随同对应视频 vid 一起入库;
2)将字幕压入视频流,主要出于两种考虑:主客端侧有各种降级逻辑,为了保证用户在端 侧播放上字幕必须出现,所以必须有一路包含硬字幕的视频流;OTT 等端还不能支持外挂字幕。
2. 优酷主客端的实现
如上图所示,优酷的播放器内核支持多个播放实例,每个实例在播放链路上由三个主要部分构成:
1)数据源处理模块(Sourcer Module):负责下载和解复用(demux);
2)解码模块(Decoder Module):对音频、视频等流的数据包进行解码,得到图像和声音等 raw 数据;
3)消费者模块(Consumer Module):同步处理,并做渲染。 目前优酷的外挂字幕是在优酷私有播放器播放内核里实现的。为了对接优酷私有播放器架构,在内核里加了单独一路 subtitle 的 pipeline。主要也分为以下三个模块:
1)subtitle sourcer:负责字幕文件的下载;
2)subtitle decoder:负责字幕的解析;
3)subtitle consumer:根据上层传下来的字幕配置信息,首先渲染成图片,然后与当前时钟做同步处理,并输出给 openrender 去做合成和渲染。
优点:
1)能有效复用内核中公共模块,比如时钟获取、利用 ffmpeg 实现对字幕的下载和解析等功能;
2)处理字幕特效比较灵活、高效。
缺点:
1)与优酷私有播放器播放内核紧密绑定,系统播放器无法支持外挂字幕;
2)与 openrender 耦合较紧,OTT 端无法支持外挂字幕;
3)目前只支持 ass 格式,可扩展性不好;
4)链路长,并且与播放内核 AliPlayer 耦合太紧,线上问题不好排查,维护成本高。
三、新架构实现
1.外挂字幕独立化重构目标:
不区分播放器类型(系统播放器、私有播放器)、实现对全平台(Android 主客、iOS 主客、 OTT 设备、mac 端等)和全机型的覆盖,同时能支持 ass 之外的多种字幕格式、支持字幕互动等创新玩法。
2.模块图
新架构主要分为以下三个模块:
1)Player SDK 除了之前与播放的处理逻辑外,加了字幕的三个模块,包括:
a)subtitle cmd:与字幕引擎的交互模块,主要是给字幕引擎下发命令。
b)subtitle msg handler:消息处理器,主要用来接收字幕引擎上抛的消息,包括每一帧对应 的字幕信息,以及出错或其他消息上报,用户上层做处理和埋点统计。
c)subtitle render:渲染模块,主要用来渲染字幕信息。
2)Subtitle Engine
整个外挂字幕处理引擎,负责 subtitle 的核心处理逻辑,覆盖除字幕渲染之外的所有功能。 后面会详细介绍 Subtitle Engine。
3)Player Kernel
渲染分两种实现方式:
a)上层渲染:把字幕信息回调给上层去做渲染,整个外挂字幕链路跟 Player Kernel 没有任 何关系。这种场景下 Player Kernel 可以不用考虑在内;
b)openrender 渲染:需要先在 Subtitle Engine 侧渲染成图片,然后从与 openrender 对接的 middle ware 层获取当前 pts,做同步之后输出给 openrender 去做合成和渲染。这种场景下,字 幕的渲染在 openrender 里面。
3. Subtitle Engine 详图
SubtitleEngine:整个外挂字幕处理引擎,负责 subtitle 的核心处理逻辑,覆盖除字幕渲染之 外的所有功能。整个 Subtitle Engine 分为以下 4 大块:
1)Msg Router 消息路由,主要负责:
a)从 Player SDK 到 Engine 的命令消息下发,包括所有的字幕消息控制逻辑,比如设置subtitle url、开始播放、暂停、恢复、停止、设置压流广告等等。
b)从 Engine 到 Player SDK 的数据消息上报,包括字幕信息的上报,错误消息、埋点信息 等的上报。
封装这样一个 Msg Router,我们就可以只实现几个简单的 API 就能实现所有功能及其扩展。 这样能有效避免之前播放内核扩展的痛点,加一个 api 就得整个播放链路一路加下去,要修改 的地方非常多,不利于扩展。
2)Common:主要是一些公共功能的封装,包括开关、配置,埋点信息,错误信息上报, tlog 等。
3)Sourcer:主要功能是点播、直播字幕文件的下载、缓存、解析。从上到下主要分为以下几个模块:
a)downloader:用于下载在线字幕文件;
b)cache:用于缓存处理逻辑。某一部剧的 ass 文件一般不会经常更新,我们在下载到本地 之后可以先缓存下来,下次用户播同样的剧可以直接使用本地的 ass 文件。当然,我们会有一 个时效及更新机制,会定期清掉本地的缓存,让用户去重新下载。本地的缓存文件也需要考虑 完整性检测机制,来保证文件的完好无损;
c)sniffer:考虑到我们要支持多种外挂字幕格式,需要一个嗅探器去探测字幕格式;
d)parser:字幕解析模块。不同格式需要有不同的 parser 实现;
e)track:对解析后字幕信息的封装,包括字幕文本,开始时间 pts,持续时长,字体配置信息等。得到这样一个 track 信息,consumer 就能获取后去消费了。
4)Consumer:主要功能是实现与当前播放进度的同步输出,各种与业务相关的逻辑都在 这里实现。比如同步、seek、跳片头、压流广告、倍速等等。
a)driver:Consumer 内部通过 driver 来驱动整个流程,driver 提供 PTS 与更新频率来驱动Consumer 与 Sourcer 交互,获取当前 PTS 下的字幕数据。pts 的获取根据渲染方式的不同有两种方式:从上层获取(见箭头(5));从 openrender 获取(见箭头(6))。
b)provider:根据从 driver 里传过来的当前 pts,去 sourcer 解析生成的 track 中获取对应的 字幕行信息。
c)line data manager:基于行的字幕数据管理模块。因单条字幕获取频率在 10~100 次/s,line data manager 内部通过缓存、预解析策略,降低与整体字幕数据的交互频率,优化了单行字幕 信息获取开销。
d)subtitle line data:从 line data manager 我们获取到对应的字幕行信息,包括字幕文本, 开始时间,持续时长,字体设置等信息。
e)两种渲染方式:
方式一:openrender。我们需要 render to image,这一步只是通过 openrender 渲染时才需要。 我们先把 subtitle line data 渲染成一张图片,然后输出给 openrender 去做渲染(见箭头(9));
方式二:上层渲染,我们在 consumer 流程中 driver->provider->line data manager->subtitle line data 获取到字幕行信息,然后转发给 msg router,再上抛给 Player SDK 去做渲染处理。
综上,整个外挂字幕的处理流程就完成了。除了 option 之前通过 openrender 渲染是与私有 播放器内核挂钩之外,其他模块、包括通过上层渲染的所有链路都与播放器类型无关了。
四、展望
在新架构下外挂字幕功能的展望:外挂字幕能在新架构下快速铺量,并且能有更多创新玩 法提供给我们的用户。
1)快速铺量:我们设计新架构不依赖于播放器类型、并且功能独立,这有利于外挂字幕功 能在不同设备上的铺量,也有利于后续的功能独立开发和升级;
2)字体、颜色、大小调整;
3)非视频画面显示:不依赖于视频播放,听剧模式下在锁屏界面显示字幕;
4)随处显示能力:可显示在视频画面里面,也可以显示在视频画面外;
5)互动能力:字幕区域可拖动,可单击,可滑动取词,可互动;
6)各种字幕特效,可以做成类似于表情包,并且可以做成会员权益,给会员客户带来更大的价值。