跨平台多媒体渲染引擎 OPR 简介
背景
在最新的优酷版本中已经支持了基于端侧实时人体识别的弹幕穿人能力,该能力大致可以分解为视频渲染模块、视频画面识别前处理模块、弹幕 mask 文件离屏合成模块、弹幕渲染模块,而这些模块正是搭载在我们构建的跨平台渲染引擎 OPR 上。其实弹幕穿人对于多媒体播放场景来说只能算较小应用之一,我们甚至可以在弹幕渲染里就罗列出更多的特效例如:3D 弹幕、多并发的动态弹幕、需要音视频信息实时配合的节奏弹幕等等;而在音视频渲染领域我们也不仅有画面识别的前处理、更多还有类似超分、插帧、音视频增强、色弱、护眼等观影模式的后处理支持。在多媒体播放这种“争分夺秒”的的场景下如何高效的实现以及组织上述功能,甚至可以实时对渲染效果做检测和统计,以及对于未来视频游戏化、互动化留有空间和技术储备,都是我们需要考虑的,正是基于这种考虑我们设计了跨平台的多媒体渲染引擎 OPR,来支撑我们的构想。
OPR 架构设计
从功能上来说我们需要将音视频前处理、后处理、渲染,2D(弹幕)渲染,3D 渲染,互动及画面检测等能力集成到一起,从特性上来说我们需要兼顾高性能、热插拔、高可维护。纵观市面成熟的引擎,其实我们找不到一款符合上述要求的,GPUImage 更多关注的是视频后处理,并且其跨平台实现需要不同技术栈;SDL 跨平台技术栈相同但是更多实现的是音视频的渲染,无法基于其实现前后处理的扩展;像 FlameMaster 更是只实现了 Android 端的弹幕渲染,局限性太大,无法扩展出炫彩的特效。深究我们提到的 2D 渲染能力、互动性等其实是一般游戏引擎具有的特点,并且视频游戏化也是未来的方向,基于此我们也需要将游戏引擎纳入我们的考虑,但是游戏引擎存在一个致命的问题就是从基因里就没有考虑过音视频后处理的事情,而后处理一般需要多个复杂算法的串并联执行,需要特殊的设计才能实现。另外考虑到渲染的高性能需求,我们需要使用 native 的 GPU 渲染,在借鉴了 cocos2D、GPUImage、SDL 等引擎的情况下我们设计出了 OPR 的基本架构,如图:
可以看出,音频的跨平台实现相对比较容易,音频后处理及渲染均是在 CPU 中完成,且大部分平台提供的渲染接口也都是基于 native 的(Android 端 audiotrack 需要通过 jni 反射 Java 层接口),同时音频处理算法复杂度和算力消耗相对视频来说均不在一个量级,这些都对我们跨平台的封装提供了便利。但是图像的处理及渲染的难度就不可同日而语,考虑到图像的计算量和并发特性我们最好使用 GPU 进行计算,而不同端的渲染协议接口和脚本语言又不尽相同,经统计如果要支持 Android、iOS、macOS、Windows 这 4 种主流系统,我们需要对至少 3 种编程语言(c++、Java、oc)和 3 种脚本语言(glsl、msl、hlsl)进行封装。如何屏蔽不同平台渲染协议的特性和语言差异设计出一套统一的流程和接口就成为了我们实现跨平台高性能渲染引擎的重点,最终我们设计出如图架构:
如上,我们基于两个维度来封装不同平台的渲染协议:渲染流程和渲染要素。对于渲染流程我们划分了渲染最小单元:renderPass,其对应了一个 render command 的执行,在实际意义上可能意味着单条弹幕的渲染。对于渲染要素在抽取不同协议的共同点之后我们形成了:buffer、shader、program、texture、env、device、utils 7 大组成。其中 env 负责本地 UI 系统与渲染协议的桥接,例如 Android 下需要 egl 连接 surfaceView 和 OpenGL ES,utils 负责将架构统一的标准翻译为不同协议各自的方式,例如 OPRPixelFormat::RGBA8888 对于 OpenGL ES 意味着 GL_RGBA,对于 metal 则意味着 MTLPixelFormatRGBA8Unorm,utils 负责保存和屏蔽这些映射关系;而 device 是一个工厂类,负责生产其他 5 大渲染要素,以此来降低不同模块对渲染要素的依赖复杂度。渲染流程通过 command 来和渲染要素进行链接,command 中的 type 用来决定是否需要微调渲染流程,zOrder 则决定了该 command 的执行顺序,blend 在描述不同 command 渲染结果的叠加方式,colorAttachment 指定了渲染结果的保存对象,programState 保存了渲染所需的要素及其对应的值。commandbuffer 通过拆解 command 上述因素做出具体的执行。最终 render 通过封装 commandBuffer 和 commandQueue 实现了基于命令流的渲染,做到了技术实现和业务彻底解耦。
基于 native UI 的弹幕渲染
上文中我们已经介绍了如何打造跨平台的音画渲染器,而图像渲染本身其实就是一个可视化的过程,所以我们对于图像渲染的能力封装就是提供可视化控件。这样做的好处有:●功能解耦,降低了单个功能开发复杂度的同时也提高了稳定性,避免了新功能的开发对存量功能的影响;●类 UI 控件的设计更符合业务同学的开发习惯,降低使用难度;●尽可能小的功能划分可以提升复用率,有利于控制代码规模和调试难度;●基于 UI 控件的交互更贴近用户使用习惯,因为我们要打造的是一款可交互的多媒体渲染引擎,而不是单纯的展示;到这里可能又带来了很多疑惑,既然我们最终提供的是 UI 控件的功能封装,为什么不直接使用原生 UI 控件或者 QT、flutter 这种跨平台的 UI 系统呢?首先是因为既然已经说明是原生则注定是生长在特定的系统里,高度依赖特定系统的上下文,当业务复杂到一定程度的时候这种跨平台的功能迁移是无法承受的;其次原生 UI 性能在高并发任务下存在不足,且大部分的 UI 还是偏向于展示型,我们提供的 UI 更侧重特效;然后就是性能,我们基于 GPU 渲染的 UI 控件可有效降低 CPU 使用和内存占用,流畅度提升。而至于 QT 和 flutter 则是有点杀鸡用牛刀的意味,并且在多媒体处理及渲染领域也不会有我们专业,我们更聚焦!回到 UI 控件的问题,我们认为其在多媒体相关的使用场景下具有三大要素:样式、布局能力、交互。样式既外观,这也是我们最擅长的领域,利用 shader 我们可以写出非常酷炫的样式,这里只有想不到没有做不到,而布局主要解决控件的位置关系、顺序关系和嵌套关系,交互意味着控件可以接受输入可以产出输出。但是一般的基于 GPU 的渲染都比较依赖上下文,这决定了我们不可能设计出真实类似于原生的 UI 系统,这里我们借鉴了游戏引擎的理念,引入了 director 和 scene 的概念。Director 可以看做是一个 timer 的主体,像 OpenGL 这种强线程要求的,我们可以利用 director 将其进行约束,确保我们所有的 OpenGL 提交都是在一个线程里,而 scene 可以看做是一个容器,容纳了需要显示的控件,切换 scene 可以实现不同页面的切换。最终综合上述因素我们设计出了如下的 nativeUI 系统:
在完成 nativeUI 系统的构建后,弹幕引擎相对来说就是水到渠成的工作了,首先创建 timer 对 director 的 render 进行驱动,这里我们可以设置常用的 60HZ,或者特色能力 90、120hz,进行按需设置。具体到单个的弹幕我们可以用 sprite 控件进行图片例如 JPG、png 的展示,利用 animated sprite 控件进行对 GIF、apng 等动图的展示,而 label 则负责文字展示,借用系统或者 freetype 我们可以完成不同字体的展示。如果我们需要更复杂的单体弹幕特效展示,则可以重新继承 node,通过控件组合或者专项开发来完成,这些操作都是简单而高效的。而整体的弹幕特效的切换则可以通过切换 scene 来实现,在正在情况下 label、sprite 等单体字幕都是以 scene 的 child 的方式进行管理,在需要整体切换至类似打 call 等特效时我们可以在普通 scene 和 effect scene 来实现平滑切换,甚至是定制过度效果。
既要又要还要的音视频渲染
上文中我们描述了如何构建基于 GPU 渲染的 nativeUI 系统,并且在该系统上跑通了弹幕能力,接下来我们就需要考虑一个更基本的场景,如何在前述的条件下跑通音视频处理及渲染能力。就视频处理及渲染而言,其不具备弹幕渲染那么明显的 UI 特性,没有复杂条目并行处理及多条目整体特效切换等需求,但这不意味着复杂度的降低,反而因为视频画面清晰度日益增长(目前移动端 1080P 已经普及,4K 在某些平台也可以看到),画面增强、风格化、插帧等功能不断涌现,不同平台软硬解、不同格式数据的兼容等都为视频渲染带来了 更大的难度。它既要保证高可复用的功能组合、动态插拔,又要保证对绝大部分机型的覆盖和最低的性能要求,还要留有未来视频互动化游戏化的余地,在整个多媒体播放的技术链路中也是属于绝对的技术高地。虽然我们提到了很多需要解决的难题,但是设计一个高性能、多功能、高可扩展的视频渲染框架仍是我们的乐趣所在。首先我们借鉴 GPUImage 进行了功能的 filter 封装,这可以解决我们链式的功能叠加,而通过 twopass filter 以及 group filter 的扩展我们更是实现了 filter 的串并联,从而实现了图式的功能复合使用。而 filter 封装了 command,承载了基本的渲染能力,这也是和弹幕渲染不同之处,弹幕以控件维度进行了 command 封装,视频渲染则更细化到了 filter 的维度。然后我们构建了 render pipeline,带有工厂属性,可以在播中动态创建 filter 插入到当前的 pipeline 中,这样我们就解决了功能复用和动态插拔的问题。现在我们需要解决的是如何使视频渲染具备基本的交互能力以及进一步的“改装”空间。在一般的视频渲染场景里,一个上屏 surface 对应一个播放实例也对应一个渲染实例,在这里的渲染实例我们可以定义为一个 videoLayer,这个 videoLayer 继承自上文提到的 node,如果需要得到鼠标点触等互动事件则可以再继承 eventLayer,而 videoLayer 内部则封装了我们提到的 pipeline,这样我们的渲染实例在整体上以一个控件的形式融入到了我们的整体 nativeUI 架构中,得到了布局、事件交互等 UI 特性,对内的 pipeline 封装则保证了其所需的复杂处理链路,最终 videoLayer 不同于其他简单控件封装一个 command,而是对外提供了一个 command 序列,通过 order 来组织其执行顺序。而对于高性能的保障是从我们的设计理念出发,贯穿于我们的实现过程,体现在各个细节。自底向上,我们从一开始就选择基于 GPU 的计算,保证了低 CPU 占用、高并发性能,在实现上我们的核心代码均采用 C++实现,保证平台复用的同时,极大的提升了性能,在细节上 pipeline o(1)复杂的的查找算法、位运算、代码块复用及性能提取都是我们为性能做出的努力,最终我们设计出如图视频渲染架构:
视频渲染构建的另外一个难点是需要兼容不同平台的硬解,iOS 的 vtb 解码可以直接吐出 pixelbuffer,可以直接呈现数据,但是类似 Android mediacodec、Windows 平台为了保障性能是不建议直接读取到内存再进行渲染。在这种情况我们构建了基于 texture 的 surfacewrap,数据直接更新至纹理,这样我们就可以提供我们的后处理能力,通过这种方式我们使得不同的系统播放器可以接入 OPR,从而另系统播放器也可以支持我们特色的护眼、超分、插帧、截图等后处理能力。
监控链路为体验保驾护航
在我们完成上述功能之后需要另外考虑的问题就是效果如何,在这里我们需要定义如何来衡量效果的好坏。一般我们认为良好的效果就是是否如实的还原了需要展示的音视频内容,并且展示过程是否流畅。据此我们规划了基于内容和基于流程的两种监控方式。对于内容监控,我们从客诉出发总结最被诟病的视频渲染异常为黑屏、花屏、绿屏等,对于音频则是音量或者静音,针对这些我们的监控系统支持按配置以一定间隔对音视频进行对应的检测。对于流程监控,我们从内存占用和平均渲染时长进行统计,其中内存我们可细化至显存、内存堆、栈的分别统计,帮助我们及时了解某部分内存的突出占用来解决内存 IO 引起的卡顿问题,而针对渲染时长的统计可以帮助我们定位是否存在某些计算量大的 filter 影响流畅度,针对上述异常我们也可以最初一些针对性的恢复措施。
未来展望
虽然我们已经完成了一些工作但是还有很多需要做的事情,例如目前我们还没有针对 Android 平台的 Vulkan 支持,对于 VR 的支持还是依赖第三方库,还需要探究更多互动和视频渲染的结合,不依赖底层开发的特效支持,简单编辑器能力等。相信 OPR 的未来会变得更好。
评论