跨平台多媒体渲染引擎OPR简介

1,144 阅读13分钟

背景

在最新的优酷版本中已经支持了基于端侧实时人体识别的弹幕穿人能力,该能力大致可以分解为视频渲染模块、视频画面识别前处理模块、弹幕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的未来会变得更好。