前两期 RTC Dev Meetup:Flutter 开发技术专场 的回顾视频已被上传至了 RTC 开发者社区,欢迎大家浏览与交流。 我们整理了其中一个演讲内容——由声网 Agora 高级架构师 张乾泽分享的“Flutter 实时音视频实践”。以下为演讲实录。
我今天给大家介绍的是 Flutter 实时音视频开发实践方面的经验。我叫张乾泽,毕业于英国牛津大学,之前在 SAP 主要担任移动端架构师,2017年加入声网Agora,目前是声网Agora 的高级架构师,主要负责 RTC 技术在娱乐、直播、教育等行业的应用研发工作。
首先我们讲到 RTC 应用,什么样是 RTC 应用呢?RTC 是 Real-time Communications。大家平时在日常生活中已经接触到非常非常多的RTC应用,比如说直播,大家平时会用手机看熊猫直播、斗鱼直播,实时看主播的游戏画面、他们唱歌的画面、跳舞的画面。还有教育,通过互联网可以实现远程教育,比如学生和美国的老师可以直接进行实时互动。
在医疗方面,我们最近也看到很多的案例。以前你要到医院挂号排队,等一上午,医生看一下,然后明天再来复诊时重新挂号,又是一个上午的时间过去了。现在有了 RTC 后,你可以通过视频直接在家里看医生,医生在视频中问你一些问题,如果病症简单,你甚至都不需要去医院。
今天我们就拿常见的视频通话的应用,来看看如何基于 Flutter 来开发实现,以及其中会有什么样的困难。
架构逻辑与实现思路
首先讲一下 Flutter 的架构逻辑。如果比较熟悉 Flutter 的朋友可能会知道,它的渲染方式跟 React Native 最不一样的地方就是它对于自己的一些原生控件,没有利用系统本身提供 View。那么开发应用的时候,怎么渲染出来的呢?
它自己里面会建立一个 Layer Tree,Layer Tree 里面是它的一个树状结构,它把数据或者渲染的地方发到 Skia,Skia 得到 GPU 的 Vsync 信号的时候,再把这些数据传给 GPU 统一渲染。完全没有通过类似 UI View 这样的 Native 的 View 的。
在知道这个概念之后,我就开始着手开发这个应用。这个应用因为它本身就提供了一些常用的组件,比如 App 的一个框架,一些按钮和导航栏,Flutter 有现成的组件可以让你直接把这些东西画上去,只需要通过一些非常简单的配置即可。
Flutter 本身也提供了与 Native SDK 的通讯的方式,叫做 Channel Massage,跟 React Native 调用 Native 的思路非常像。所以我们有了声网 Agora SDK,会帮我们处理音频编解码、音频的传输,然后我们可以利用 Flutter 本身的组件把 UI 也画好,好像这个东西就 Work 了。当一切都似乎变得那么美好的时候,我卡在了视频上。Flutter 怎么渲染视频?
在 Native 平台都有系统组件来渲染视频,但 Flutter 没有这样的东西。我现在要把实时视频渲染出来。
于是我查看了 Flutter 的源码,其中有一个组件叫 Video player,这是放视频的。其中有一个 Texture Widget,它在这种模式下提供了一种机制,可以让你进行视频的渲染。
实现思路一:Texture Widget
首先视频是由一帧帧图像组成的。Flutter 的 Texture 提供了一个可以放在 Layer Tree 里的组件,组件中的数据源需要由你通过 Native 端来提供。
在这里以 iOS 为例,iOS 就需要提供 CVPixelBufferRef。这是一个数据,对应的就是视频中的一帧画面。把这个数据作为数据源提供给Texture Widget,然后Texture Widget就可以把你提供的这些数据显示出来,最终就变成了一个视频。
上图是大致的实现架构。首先要在 Dart 上实现一个 Native 的 Plugin。Plugin Registar是Plugin的一个入口,有一个属性叫做 TextureRegistry,你可以在这里注册一个你自己的Texture类,这个类可能实现了一个协议叫Flutter Texture。你实现这个协议过程中有两个方法需要实现:一个方法叫做 CVPixelBufferRef,就是要提供数据源到你的Texture。另一个方法你需要自己主动去调,叫做textureFrameAvailable,它的作用是告诉 TextureRegistry 现在可以根据提供的数据来更新画面了,就是我提供数据。
其中有一个问题,现在有 TextureWidget,如果更新数据源的话,它怎么才能知道我要更新哪个Texture呢?其实,在注册这Texture的时候,它会返回一个 TextureID。你在创建 Texture Widget 的时候需要把这个 TextureID 传进去。这样的话 Texture 的组件就会跟你提供的这个实践进行绑定,知道你提供的数据源就是为这个 Texture 提供的数据。以此类推,我们也可以有很多个 Texture。
回到我们之前的设计来看(如上图),我们这边是一个视频通话应用,我们有很多本地的视频,有很多的远端的视频,这些视频都可以是不同的 Texture Widget,放在Dart的页面容器中,然后我们分别为它们提供数据源,进行数据渲染。
是不是感觉现在一切准备就绪了?其实还有一个问题。Flutter官方的 Github 提供了一些示例代码,但是它提供的功能是让你播放一个静态缓存,我们这里提供的是一个实时的视频流,不是一个视频文件。不可能以读取一个文件的方式,把里面的数据一帧一帧地转化之后,传给 Texture Widget。我们需要有一个方法拿到这些实时的数据,把它作为数据源传给Texture。
这时候我们就需要使用声网Agora SDK。首先给大家普及一个概念,大家想象一下平时在看一个直播的时候,直播那边有一个摄像头,他玩一些游戏,他跳一些舞、唱一些歌,是怎样让我在手机里面看到这些数据的呢?总结来讲是一个非常简单的流程,如上图所示。首先主播有一个摄像头,进行数据的采集。这些数据是裸数据。在拿到这些数据后,我们需要进行编码。在编码过程中,我们可以对数据进行处理和格式化,比如说让它有一个特定的分辨率和帧率。直播不会把摄像头的所有的数据都传过来,他会对数据进行压缩、处理,这就是编码的过程。在编码完成后,我们就有一个视频数据,它会通过声网的云服务,快速传输到你的手机上。手机客户端上也有声网的 SDK,在拿到数据后,会把数据进行解码,并提供一个方法,将这些视频数据传到UI View 上,最终显示为你所看到的视频。
如上图所示,基于刚才的架构,我们添加了一个声网 SDK。SDK 有一个方法叫做 AgoraVideoSink。它提供了一个回调,会把收到的所有视频数据按照你想要的格式传给你。我们在得到回调数据后,再把他传给Flutter Texture。我们可以设定一个更新频率,每当得到一次回调后,通过 notify 告诉 TextureRegistery 将新的数据更新到 Texture。通过这样的一个过程,我们就能实现一个 Flutter 的实时音视频 App 了。
实现思路二:PlatformView
由于 Texture 会涉及到很多渲染的流程,所以很多人都会觉得它有些复杂。所以在 Flutter 1.0版本中,Google 给出了一个新的东西,叫做 PlatformView。
它提供了一种方法,让我们可以创建 UI View,并加到 Dart 的 LayerTree 里。在 Dart 中的类对应到 iOS 和 Android 平台分别是UIKitView 和 AndroidView。
PlatformView 该怎么用呢?在 PluginRegistar 中新增了 ViewFactory,ViewFactory 只有 CreateView 这一个方法需要实现。你可以在这个方法里首先提供一个 Identifier,在实现该方法后,可以返回一个你想要的 PlatformView,并与 Dart 组件绑定在一起。
因为我们的 SDK 支持传递 Native 的 View,然后将视频渲染到上面。如上图,我们基于刚才的架构做一下改动。声网 SDK 中提供了一个 CustomViewFoctory,它的作用就是创建一个原生的 View,然后 SDK 的 AgoraRtcEngine 会与 View 绑定,在收到数据流的时候会自动将它渲染到这个 View 上。因为每个 View 会对应一个 ViewID。那么我们只需要通过 ViewID 就可以得到这个 View,然后把它渲染出来。
性能对比
总结一下,在 Flutter 中实现实时音视频有两种方式,一种是 Texture,另一种是 PlatformView。我们将它们与 Native 的实现方式进行了性能对比,结果如下图所示。
三者相比,Texture 的实现方式性能比较差。PlatformView 的性能与 Native 相近。就目前的 Demo 来看,造成Texture 的方法性能较差的原因可能有两个:
第一,在收到视频数据之后,会立刻更新 Texture,但这样做的必要性不大,如果进行一些优化的话,还可以提升一些性能;
第二,我们是通过一个回调来获取数据,然后将它提供给 Flutter 的 Texture,这个过程是通过 CPU 来完成的,但通常来讲,渲染的工作需要由 GPU 来做,所以数据从 GPU 到 CPU 又再到 GPU,这个过程非常耗费资源。
总结一下,我个人对于 Texture 和 PlatformView 的优缺点比较。我个人觉得Texture这个东西实现“更Dart”,什么叫“更Dart”?就是没有很多原生的参与,比如 UI View ,更符合 Dart 的生态环境,在设计上更加纯粹。但是它也有缺点。如果用它做一些事情,不做特殊处理的话,可能很难避免 CPU、GPU 中间数据拷贝,造成性能损失。如果你要用它去做一个地图,那你就得把地图数据渲染放在Flutter上,用Flutter的逻辑全部重新实现一遍。这个对于大多数开发者来说,对于正在蓬勃发展中的一个生态来说是非常不利的。所以我个人认为这也是 Google 在 Flutter 1.0加入了PlatformView 的原因。
使用 PlatformView 有什么优点呢?我刚才说了,它可以把之前实现 Native View 的功能直接放在Dart里。对于开发者来说,更加简单易用。也可以避免前者存在在 CPU、GPU 数据拷贝带来的性能损失问题。但问题,它在设计上不那么纯粹,会引入一下不可控的因素。
详细 PPT 与回顾视频,请点击这里获取