基于 Flutter 的移动中间件体系和音视频技术

2,186 阅读17分钟

招贤纳士​

我们急切需要浏览器渲染引擎/Flutter 渲染引擎的人才,欢迎大牛们加入我们

前言

1 月 16 日,UC 技术委员会联合掘金、谷歌开发者社区举办了 2021 年首届 Flutter 引擎为主题的技术沙龙活动。活动吸引了 150 多名同学报名,由于疫情控制现场人数的影响,最终我们只能安排 50 名同学来到现场。另外,有 2000 余名同学围观了现场直播。活动中,来自阿里巴巴集团内的五位技术专家与大家分享了基于 Flutter 构建的研发体系,开发与优化经验,动态化方案,以及 UC 定制的 Flutter 增强引擎 Hummer 的优势与新特性。

第一场分享是由 UC/夸克客户端负责人、UC 移动技术中台负责人辉鸿带来的《打造基于 Flutter 的 UC 移动技术中台》。

第二场分享是由 UC Flutter Hummer 引擎技术负责人佬龙带来的《Hummer (Flutter 定制引擎)优化及体系化建设探索》。

第三场分享是由 UC 浏览器视频技术专家礼渊带来的《基于 Flutter 的移动中间件技术体系和音视频技术》。

分享内容

大家好,欢迎大家参加今天的分享,我是 UC 信息流团队的礼渊。今天我会给大家介绍基于 Flutter 移动中间件体系以及其中的音视频技术。 

今天的分享内容主要分为四个部分,首先我会给大家介绍建设 Flutter 移动中间件的思路;然后会重点展开其中如何去构建视频播放、编辑能力;最后会说说未来的规划。 

在建设移动中间件的场景里面,我们主要考虑该如何去更快赋能【特定航道】的创新业务探索?

我们的核心思路是将中间件拆分为通用层和业务层去建设,这样可以进一步提升效能。接下来介绍中间件的应用场景:

1、UC 浏览器内的内容业务:我们抽象这些业务的共性,沉淀出一批基于信息流架构的业务中间件,并且高速孵化了端内的垂类新场景。在实际迭代过程中降低了 70% 以上的成本;

2、UC 内的创新 APP:针对这种场景的处理方式跟端内场景类似,但是后端采用的是统一的中台架构;

3、其他 BU 的使用场景:其他 BU 也有需求使用类似的能力,但会有自己的后端,所以我们在业务中间件下沉淀出一批通用中间件;

接下会重点介绍在多媒体领域的建设情况,我们分三层去构建多媒体的能力:

1、外壳层负责解决业务痛点、降低能力接入成本以及支持业务功能差异化定制。在这里有 Auror 插件播放器和 Kaleido 多视频编辑器。

2、通道层的实现除了使用常见的共享纹理以及 methodchannel 外,还会去探索性能更高的 ffi 通道;

3、内核层负责沉淀通用多媒体能力,统一采用 C++ 构建。在播放侧使用 UC 的 Apollo 播放内核,同时也支持其他业务方切换到自定义的内核上。在编辑侧使用的是 LLVO 编辑引擎,LLVO 除了支持上面的 kaleido 多视频编辑器外也会在其他场景支持拍摄、转码等功能。

我们是如何打造一个统一架构并且高扩展性的 Flutter 播放器去支持多种多样的播放器使用场景的?

给大家介绍一下使用播放器场景的现状:

1、外壳层:不管是多个 APP 内或者一个 APP 内的多个业务场景都可能有独自的播放外壳;而这些外壳通常都没有预留扩展性,功能扩展经常需要改动框架;同时这些播放外壳都是采用纯平台层代码去开发,功能虽然都差不多,但是无法避免重复开发。

2、内核层:UC 使用 Apollo 内核,但业务方也有使用其他播放内核的需求。

我是如何用 Flutter 打造一套通用的播放外壳来彻底解决不同业务场景的复用和定制问题的?接下来会主要从这四个方面去介绍。

播放画面渲染到 Flutter 上常用的技术方案有 platform view 和共享纹理两种。在性能方面,共享纹理因为无需合并 UI 与 Platform 线程,所以有更高的性能。但采用共享纹理因为要重新设计、开发外壳,所以前期的迭代成本会比较高。但其提供的跨平台能力会在后续迭代中有更好的复用性,综合考虑我们的业务场景,我们最后采用了共享纹理的方案。

在内核选型方面,我们决定把选型权交回给业务方。在平台层提供一个播放设配器,接入 SDK 只要实现这个适配器的接口就可以,无需关注内部细节。

接下来介绍我们是如何通过抽象业务之间的共性去实现一个统一架构、并且支持灵活定制和扩展的播放外壳的。右边四张图是我们使用播放器的业务场景,可以看见这些场景中外壳的差异点主要在布局结构以及控件样式上面,在最后一张图里面也会有一些如关注、分享的自定义功能。当然它们之间也存在着共性。 为了能沉淀场景的共性以及支持业务方的差异化定制,我们实现了一套插件化播放外壳。

1、首先是插件定义,插件是播放面板上的一个 UI 元素,它们都基于通用播控管理及消息接发机制,在外壳框架无法满足插件功能诉求时,支持注册独立的业务管理器。

2、使用插件前要先对其进行配置,在这里要先构建一个插件 tree,这个 tree 负责描述插件之间的层级关系,允许插件有自己的子插件,这种设计为了方便实现多个插件之间的整体动画效果。

3、插件运行前可以传入自定义样式,样式描述常用播放控件属性,如 titleBar 的字体颜色,播放按钮图标之类。插件会根据配置展示在播放面板上。在底层抽象了常用的播放功能,播控状态统一由 redux 管理,插件按需监听。插件与插件之间结耦,统一 EventManager 处理消息接发。

除了实现插件化外壳外,我们也极力优化播放场景的性能。视频卡片列表是信息流内最常用的视频播放形态,接下来主要介绍如何在这个场景下实现视频秒开。

右上角是从列表滑动到显示视频画面过程中涉及的任务列表,主要的优化思路是提前执行其中的耗时任务,在播放当前视频源时预先加载下一个视频源:

1、首先是播放内核支持预先下载 200~300KB 的数据,具体大小根据服端下发的平均码率而定,应该要保证内容能播放 100~200MS。

2、接下来内核会利用预下载的数据去初始化解封装器、解码器等资源,并接视频首帧渲染出来。

3、加入预加载后,创建、销毁播放器的频率会提高,所以加入实例内存池,避免播放器重复创建耗时。

4、在快速滑动的场景下,没有必要预加载每张卡片的视频内容,在这里我们加入了目标检测机制,根据列表滑动速度来判断是否需要预加载视频卡片内容。

5、最后守护者机制为了降低预加载对应用内存及服务端的影响,在发现内存水位过高或收到服端发出的 QPS 警告后会对应缩小内存池容量和降低预加载请求量。

完成一系列的优化后,平均开播耗时从 561ms 降低到 118ms,体感完全达到直出效果。

接下来会介绍如果在 Flutter 上构建音视频编辑器。 

近年来围绕视频内容生产的工具都频繁被资本看好,侧面上也证明了这个赛道足够的大。而在 UC 内大对多视频编辑的需求不断提升,作者对编辑性能、生产视频质量能因素也在不断提高。在功能实现方面无论是编辑交互界面还是内存能力的构建都十分复杂,我们希望使用 Flutter & C++ 的技术选型来彻底解决多端重复开发的问题。

综上现状,我们面对的挑战主要分为功能和性能两个方面,接下来我会展开介绍其中的内容。

首先介绍一下什么是多视频编辑?在视频编辑前支持多个多媒体素材,可以是视频文件、音频文件,也可以是图片。在导入素材后编辑框架会以图示的结构去管理素材,其中有两个重要的概念,一个是片段:片段映射素材里一段指定时间范围的内容;一个是轨道:轨道里面包含多个片段,轨道分为两个类型,一个是视频轨,另一个是音频轨。素材内容经过框架解码、特效加工、轨道融合后会将音视频数据交给上层的高级功能。在编辑阶段支持对片段做裁剪、分割、调序以及添加各种空域、时域特效,所以编辑效果均可实时预览。

这是 Flutter 实现的编辑外壳。在顶层封装了一系列的编辑 UI 组件,业务方可以使用这些组件来搭建各自的编辑界面;在 UI 组件层之下有两个重要的模块,分别是任务管理和状态管理模块。其中任务管理负责将内核提供的原子操作封装成一个高级任务操作,如图示当删除片段 A 后,需要同时移动片段 B、C 的位置,这时候调用一个高级任务就能完成操作,因为任务管理模块与内核的交互频次较高,所以采用 FFI 直接调用 C++ 方式代替传统调用 methodChannel 方式,这样可以避免消息在多线程间传递带来耗时。另一个是状态管理模块,以 stack 的形式存放每次编辑后的片段、轨道结构信息,编辑中可以用于撤回、恢复等操作,也可以交给草稿箱模块做持久化。

这是 kaleido 的整体架构。上方是刚才介绍的外壳层;底部的引擎层主要分为两层,上层包含如预览器、截图生成、还有合成器这些高级编辑功能,下层是多视频编辑的核心模块,主要有多片段、多轨道解码调度模块和音视频特效加工、融合模块。接下来会详细介绍在实现多视频编辑器中遇到的一些核心挑战。

Seek 是编辑中的高频操作,该如何保障画面更新实时性?视频帧分为三类,分别是 I、B、P 帧,其中 B、P 帧无法独立解码而且数据比 I 帧多很多,在 Seek 的过程中命中 B、P 帧的概率会更高。因为这个原因导致 Seek 过程解码速度普遍无法跟上渲染速度。

1、先看看常规的优化。在一个 GOP 里面会存在一些非参考帧,其他帧解码时不会依赖这些帧,所以在图示的情况可以直接跳过中间的非参考帧;另一个是当使用 ffmpeg 或系统多媒体 API 时,触发 Seek 函数一般会跳到指定时间前的一个关键帧上面,在图示的情况下也会出现重复解码的问题,所以应该先查询当前帧和目标帧附近的帧信息再决定是要继续解码还是触发 Seek 函数。

2、另外我们也针对不同的场景做优化。用户通过缓慢拖动 SeekBar 调整编辑效果,这种情况下会优先查询缓存队列,如果存在可用的帧就立刻更新画面。缓存队列是为了提升框架并发性而设置的一种机制,队列分为两个区域,回收区存放使用过的帧,而预加载区存放已经解码但并未使用的帧。当发现 Seek 命中回收区,且回收区可用数据不足时会调整两个区域的大小,并且解码线程反向填充缓存队列。

3、在用户快速拖动 SeekBar 的情况下,由于用户对画面的准确性要求降低,所以这时会给每个 Seek 任务设置一个超时时间,超时后将用最后解码帧更新画面,同时要保证画面变化方向与 SeekBar 拖动方向一致。这样在拖动 SeekBar 过程中画面能连续快速更新,在主观体感上能提升流畅度。

SeekBar 里会显示视频的预览缩略图,可以帮助用户快速定位视频内容,那该怎样提升缩略图的加载速度呢?

上图是常规的加载流程,可以看到这个流程虽然简单,但是存在一些问题。下图是优化后的方案,在 Flutter 侧将缩略图列表分为可见区域和预加载区域,这样可以预先加载将要显示的内容。当缩略图发出的加载请求来到 Native 侧后会先查询纹理缓存池中是否有可用的纹理,如果没有则将请求交一下步进行解码、渲染,在这之前会先对请求做预处理。预处理包含两个方面:

第一是重排请求顺序,尽量保证解码器能继续解码;

第二是查询每个请求指定范围内有没有可用的关键帧,因为在编辑中预览缩略图对应的是一个时间片,而不是一个时间点,增加范围查询可以有效降低解码耗时。

最后使用完的纹理会被回收,避免内存过大导致崩溃。

采用系统硬件编解码去加速视频合成是业界常规的做法,但硬编解码也存在一些问题。我们实际的业务场景对视频合成画质、大小、速度有着不一样的诉求,所以单纯使用硬编解时无法满足需求的,那应该怎样去实现合成器呢?为了能满足各个业务方不同的诉求我们实现了一套设备自适应的编解码机制。在客户端上会自动执行各种软硬搭配和编码档位组合的 Benchmark,并且记录每个流程的最终得分,当需要使用相应资源时会根据场景需求获取最佳参数。

既然添加了软编辑器后,该如何最大发挥它的潜力呢?在性能方面除了调优软编解码参数外,我们发现其最大的性能瓶颈在渲染阶段。上图是常规的使用流程,可以看见无论是解码侧还是编码侧都存在一些耗时操作,针对这些耗时操作,主要的优化手段是采用 GPU 去转换格式,同时采用性能更高的数据传输方式。在解码侧是采用 PBO 去传输数据,这里应该注意可能出现的兼容性问题,所以使用前应先比较传统方案的速度,在 GPU 收到 YUV 数据后再用 Shader 去转换出 RGBA 纹理;在编码侧采用兼容性更好的 ImageReader,由于 OpenGL 输出都是 RGBA 数据,我们可以像图示例一样存放 YUV 数据,值得注意的是应该尽量输出 UV 交错的格式数据,这样可以保证对 RGBA 数据做一次采样后同时计算出 UV 值,相比 UV 不交错的数据能提升一倍的性能。

在视频生产场景中,视频合成和发布功能往往共存,但两者通常都是串行执行,我们希望并发执行这个功能来提升视频发布整体速度。但在实现中往往会遇到图示的问题:合成进程中途修改已经上传的内容。要支持知道为什么出现这种情况,首先要了解 MP4 的结构,MP4 由多个 Box 组成,ffmpeg 在封装视频时会将先将内容填充到 Box 的 body 中,最后在 header 写入内容大小。mdat Box 里面存放着编码后的媒体数据,一个 mdat 可以作为一个最小的上传分片。但 ffmpeg 内通常采用单个 mdat 去存放所有媒体数据,所以可以基于 ffmpeg 做优化,用多 mdat 结构去代替单 mdat 结构,这样就可以实现边合成边上传。 

现在移动设备的硬件发展迅速,超高清的摄像头也逐渐成为标配,对应相册里面的视频分辨率越来越大。一个 RGBA 格式 4k 视频帧达到 30MB,而 YUV 格式也去到 10MB,而编辑框架为了提升并发性,往往会有各种缓存队列,那代表同一时间有可能存在各个音视频帧,那该如何去优化编辑中的内存占用大小呢?

1、首先在解码侧应该优先使用硬件解码器,因为内存主要分布在多媒体进程上,所以对当前进程的内存影响较少。

2、针对相同源的片段应该复用解码器。

3、因为在 Android 平台上对硬解器实例数量有限制,所以还需用到软解码器(ffmpeg),而软解码器内部会维护一个帧内存池,这个内存池是 ffmpeg 减低内存抖动的一种优化手段,但大小只增不减在多视频编辑的场景里面不够适用,所以应该在不同源的两个片段过渡后立即释放内存池资源。

4、还有在预览场景,预览画面的分辨率可能会比实际视频的分辨率小,针对这种场景可以提前缩小视频帧,并且立即释放原始视频帧占用内存。

5、在渲染侧我们封装了一系列的常用基础 GL Fliter,这样可以避免开发特效时因不合理的实现导致内存被滥用。

6、最后是通过构建 GL 内存池去降低内存占用总量,共享上下文的场景需要资源时均需从内存池中申请资源,并且使用完后应立即将资源回收到内存池中。

最后是我们的未来规划。

1、首先我们希望在编辑侧针对部分高端机型尝试落地 H.265,相比目前使用的 H.264 有更高的压缩率,那就是说在生成同样画质的情况下视频的大小会更小,但其中的挑战是解码速度也会相应降低。

2、在架构相对较简单的播放侧会试水更新一代的 AV1。

3、业界往智能化移动生产方向的发展越趋明显,未来用户使用移动生产工具的门槛将会不断减低。在这个方面我们会考虑结合业务现状去实现智能一键成片和自适应编码两个功能。

4、目前社区中开源的多媒体开发库数量相对较少,我们希望能独立出一套音视频开发库,开源回馈社区。  

关注公众号请搜索 U4内核技术,即时获取最新的技术动态