阅读 1967

设计、开发一个 Flutter Plugin 的实践心得

作者:郝阳,声网Agora 工程师

如对我们的Flutter插件开发过程感兴趣,或遇到实时音视频相关开发问题,欢迎访问声网 Agora问答版块,发帖与我们的工程师交流。

应开发者们的需求,我们在推出了 Agora Flutter SDK,它以 Flutter Plugin 的形式为 Flutter App 增添实时音视频能力。同时,我们也给出了一个 Quickstart Demo

考虑到 Flutter 对于部分开发者来讲,仍是个新鲜事物。所以,我们也分享了来自 RTC 开发者社区作者的 Flutter 开发经验。其实,在开发 Agora Flutter SDK 之初,我们的技术团队也就如何基于 Flutter 实现实时音视频,深入做过一番调研。本文将就调研的过程和成果,为大家分享一些我们的经验。

Flutter 如何调用原生代码

我们要做的是在 Flutter 上实现实时音视频。那么在开始具体的工作之前,首先需要了解 Flutter 是如何调用诸如“获取媒体设备”这类原生平台 API 的。

上方来自官方的架构图已经足够清晰了,Flutter 通过 MethodChannel 发起某一方法的调用,然后原生平台收到消息后执行相应的实现(Java/Kotlin/Swift/Object-C)并异步地返回结果,以 getUserMedia 为示例,首先在 Flutter 层中声明这一方法,具体实现则是通过 MethodChannel 发送一条携带调用方法名和相应参数的信息。

Future<MediaStream> getUserMedia(
  Map<String, dynamic> mediaConstraints
) async {
  // 获取事前统一注册好的 MethodChannel
  MethodChannel channel = WebRTC.methodChannel();
  try {
    // 通过该 MethodChannel 去调用对应的方法 getUserMedia
    final Map<dynamic, dynamic> response = await channel.invokeMethod(
      'getUserMedia', // 方法名
      <String, dynamic>{'constraints': mediaConstraints}, // 参数
    );
    // 基于异步返回的结果完成封装一个在 Flutter 层使用的 MediaStream 对象
    String streamId = response["streamId"];
    MediaStream stream = new MediaStream(streamId);
    stream.setMediaTracks(response['audioTracks'], response['videoTracks']);
    // 返回结果
    return stream;
  } on PlatformException catch (e) {
    // 处理异常
    throw 'Unable to getUserMedia: ${e.message}';
  }
}
复制代码

Future 表示一个异步的调用,类似 Javascript 的Promise;async/await 类似,在一个async 函数中,会类似同步地按顺序去执行 await 方法,尽管 await 后面的是异步方法。

当平台在 MainActivity 中同样注册 MethodChannel,通过 MethodChannel 收到方法调用的消息和参数后,基于本平台实现相应逻辑,并返回执行结果,此处仅以 Android 平台为例:

// 注册 MethodChannel 接收 Flutter 的调用
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "FlutterWebRTC";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
		// 注册 MethodChannel,ChannelName 应与之前 Flutter 中注册的同名
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
        		// 提供各个方法的具体实现
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // TODO
                    }
                });
    }
}
复制代码
// 具体实现
@Override
public void onMethodCall(MethodCall call, Result result) {
	// 如果方法名为 getUserMedia
	if (call.method.equals("getUserMedia")) {
	    // Android 实现 getUserMedia 方法
	    // ...
	
	    // 成功则返回相关信息
	    // result.success(// 相关信息);
	
	    // 如果失败则抛出异常
	    // result.error(// 报错信息);
	
	} else {
	    result.notImplemented();
	}
}
复制代码

更多详细的信息可以参考 Flutter 官方示例与解释

实现音视频 SDK 的思路

了解上述 Flutter 调用原生平台方法的原理后,我们就有两种思路来实现一个音视频 SDK。

1. 先在原生平台实现音视频 SDK,后 Flutter 通过 MethodChannel 直接调用 SDK 提供的方法。

具体的方案为直接通过 MethodChannel 调用已有的声网Agora SDK,并在 Flutter 层抹去可能存在的差异,诸如参数不同、部分方法名不同。

这种做法的主要优点在于可以最大程度复用已有的 SDK,类似于建立了一层桥接。

2. 先基于原生平台实现 WebRTC 标准,然后在 Flutter 层通过 MethodChannel 调用 WebRTC 接口,再实现音视频 SDK 逻辑。

这种方案先利用原生平台实现 WebRTC 标准(前一节实现的getUserMedia就是此标准的一部分),然后在 Flutter 层注册为 WebRTC Plugin。在这个 Flutter WebRTC Plugin 的基础上参照声网音视频 SDK,连接到 Agora SD-RTN™ 全球虚拟通讯网络。

这种方案相比前一点,相当于实现一个全新的 Dart 语言的 SDK,需要用到更多 Dart 的标准库(诸如math、io、convert之类)与第三方生态(如(flutter_webrtc)。假设要支持更多的平台时(比如 Windows),只需要该平台实现 WebRTC 标准就可以直接使用。

熟悉 WebRTC 的同学们可能知道在实现浏览器 WebRTC 应用的时候有一个Adapter 的概念,目的就是为了掩藏几大主流浏览器 WebRTC 接口的些许差异,和本方案的思路是类似的,只不过适配的平台从 Firefox/Chrome/Safari 变为了 Windows/iOS/Android 等。

最终出于调研的目的,同时也是为了更加迎合 Flutter 一套代码,多平台通用的思想(理论上 SDK 就是一层设计完备的客户端逻辑,在 WebRTC 受良好支持的情况下,工作的内容就变为:如何使用 Dart 语言在 WebRTC 的标准上实现音视频通信逻辑),我们选择采用这个方案,因此读者可能会发现这个 Flutter SDK 整体上不少概念上更接近于声网 Web 平台的音视频 SDK 一些。

SDK的结构

SDK 的主要功能大致包含了音视频采集与播放,与 Agora Gateway 建立 P2P连接并管理,以及与 Gateway 之间的消息交换和处理。

虽然 Flutter 社区相对较新,但是 Dart 的标准库可以算得上是非常完备了,同时也已经有不少优秀的第三方 Plugin 。

代码可以主要拆分为以下模块:

基于 dart:io 中 Websocket 相关的方法实现与 Gateway 之间的消息通信(比如publish/subscribe这类消息和回复)

基于开源社区的 flutter_webrtc 项目实现音视频采集以及 p2p 连接等 WebRTC 相关功能

基于 dart Stream 对象或是简单的 Map 来实现 EventEmitter 这些 SDK 所需的辅助类(当然也可以直接采用 Dart 的 Stream/Sink 概念进行替代)。

这些模块完成后,在此之上就可以实现类似声网 Web SDK 中的 Client 与 Stream 对象。

其中值得一说的是视频流的播放,可以借助 flutter_webrtc plugin 中的 RTCVideoView 对象来实现,想要深入了解具体原理的可以学习一下 Flutter 外接纹理 (Texture) 相关概念。

到此 SDK 就已经基本形成了,之后便是 UI 层的开发,Flutter 这一部分很大程度上受到了 React 框架的启发,熟悉该框架的 Web 开发者可以基于此 SDK 轻松的实现一个可运行在 Android/iOS 平台的视频通话 App。我们此前分享过的 demo 已经成功和已有的声网 Android/iOS/Web SDK 进行互通,相应的代码也许将在不久未来进行开源。

总结

尽管 Flutter 社区仍然很年轻,但是已经逐渐有不少优秀的第三方插件涌现出来,加上 Dart 相对全面的标准库,实现这样一个音视频 SDK 或是类似的功能并不需要自己大量地去造轮子,加上 Flutter 本身环境搭建/构建/调试都非常的方便,因此整个开发过程中几乎没有遇到什么坑。

此外在应用层的开发过程中,风格非常接近于使用 React 进行 Web 开发,加上 Flutter 亚秒级的 Hot Reload 等特性,在开发体验与效率上相比原生开发确实有着不小的优势。

再考虑到逐渐完善的跨平台特性(桌面端的 flutter-desktop-embedding 项目与浏览器端的 humming bird 项目)以及可能会到来的谷歌新操作系统 Fuchsia,对于无论是想要接触到原生开发的 Web 开发者,还是追求更高的开发效率和更好的开发体验的原生开发者来说,Flutter 都是一个非常适宜的切入角度,值得在新的一年里加入自己的技术栈中。


想学习更多技术团队的 Flutter 开发经验?3月23日,RTC Dev Meetup 北京站 邀请LeanCloud、声网、大麦网、美团点评技术团队的工程师与你分享更多点击了解详情

关注下面的标签,发现更多相似文章
评论