設計、開發一個 Flutter Plugin 的實踐心得

聲網Agora發表於2019-03-06

作者:郝陽,聲網Agora 工程師

如對我們的Flutter外掛開發過程感興趣,或遇到實時音視訊相關開發問題,歡迎訪問聲網 Agora問答版塊,發帖與我們的工程師交流。

應開發者們的需求,我們在推出了 Agora Flutter SDK,它以 Flutter Plugin 的形式為 Flutter App 增添實時音視訊能力。同時,我們也給出了一個 Quickstart Demo

考慮到 Flutter 對於部分開發者來講,仍是個新鮮事物。所以,我們也分享了來自 RTC 開發者社群作者的 Flutter 開發經驗。其實,在開發 Agora Flutter SDK 之初,我們的技術團隊也就如何基於 Flutter 實現實時音視訊,深入做過一番調研。本文將就調研的過程和成果,為大家分享一些我們的經驗。

Flutter 如何呼叫原生程式碼

我們要做的是在 Flutter 上實現實時音視訊。那麼在開始具體的工作之前,首先需要了解 Flutter 是如何呼叫諸如“獲取媒體裝置”這類原生平臺 API 的。

設計、開發一個 Flutter Plugin 的實踐心得

上方來自官方的架構圖已經足夠清晰了,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的結構

設計、開發一個 Flutter Plugin 的實踐心得

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、聲網、大麥網、美團點評技術團隊的工程師與你分享更多點選瞭解詳情

相關文章