背景
在最新的优酷版本中曾经反对了基于端侧实时人体辨认的弹幕穿人能力,该能力大抵能够合成为视频渲染模块、视频画面辨认前解决模块、弹幕 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 的将来会变得更好。