flutter插件封裝阿里音視頻服務sdk

由于阿里音視頻服務目前僅提供android和ios的原生sdk,但我們的項目準備用flutter。so需要封裝一個flutter插件, 這里記錄下封裝過程,方便以后快速回憶。

0.基本需求
  • 提供基礎音視頻通話畫面。
  • 提供本地攝像頭預覽功能
  • 能夠加入退出頻道,發(fā)布本地視頻推流,和訂閱遠端其他用戶視頻流。
  • 能夠在所有訂閱用戶和本地視頻流之間進行畫面切換。
  • 擴展 其他業(yè)務相關功能在flutter層做。

通過sdk文檔發(fā)現(xiàn)只需要將用于播放視頻的視圖提供到flutter即可,ios是AliRenderView,android端是SophonSurfaceView。

1.使用工具

VSCode, Xcode, Android Studio

VSCode用于編輯插件工程,主要是編寫flutter端插件代碼以及測試demo代碼。
Xcode和Android Studio分別用來編輯對應平臺的原生插件代買,為了利用其代碼檢查,與調試。

2.創(chuàng)建插件工程

貌似默認語言是swift和kotlin, 用-i和-a指定為oc和java

flutter create --template=plugin -i objc -a java DaqoRTC

工程目錄如下:

B9B2E5A2-D8EC-4B1F-ADC7-3F1FA3039925.png

其中android目錄存放插件的android原生代碼
ios目錄存放插件的ios原生代碼
lib目錄存放插件的flutter代碼
example目錄是一個flutter工程,使用該插件的flutter demo

重點:example項目中的ios和android目錄才是編輯插件原生代碼的地方,并不在上面說的那兩個文件夾中直接編輯。分別用android Studio和xcode打開example中對應的android和ios工程。再此之前按照官方文檔的說法先buid一下
// android
cd DaqoRTC/example; flutter build apk


// ios
cd DaqoRTC/example; flutter build ios --no-codesign

貌似是讓gradle和cocoapod同步下各自的工程。如果有使用三方sdk這個時候會去對應倉庫獲取。在build之前先進行sdk配置
配置方法如下:

  • android


    2690FD93-6F1C-417B-BD5E-2BF600DACF42.png

在DaqoRTC文件夾下還有個libs文件夾,將三方sdk放在里面,這個文件夾在android studio中看不到?jīng)]關系。圖中com.example.DaqoRTC目錄下就是插件java源碼。

  • iOS


    71895692-C928-4D59-8BCB-E597EB0E866A.png

ios在build的時候會從pod倉庫中下載,不用將sdk放到本地。

這樣等pod和gradle構建完成之后就可以分別在ios的Classes目錄和android的com.example.DaqoRTC目錄下愉快的編寫插件代碼了。然后在各自的工程中直接build測試就行了。
不要忘了在example/lib/mian.dart中添加插件的測試代碼。

還有插件的dart代碼直接在插件工程的lib目錄中添加,注意不是example中。

B8FE7181-AE09-4397-ABD0-FEFF148207E8.png
3.插件的實現(xiàn)步驟

按照flutter要求提供三個主要文件:
DaqoRTCPlugin
DaqoRtcViewFactory
DaqoPlayerController

  • iOS

1.一個實現(xiàn)FlutterPlugin協(xié)議的DaqoRTCPlugin類,實現(xiàn)其registerWithRegistrar靜態(tài)方法,這是插件注冊的入口,app在啟動的時候會調用。據(jù)地調用地方是:


EA70D678-953B-4965-9EA7-A92BBE8352C4.png
9708074E-2A91-4A1D-8877-876034923282.png
  1. 第二個是實現(xiàn)FlutterPlatformViewFactory協(xié)議的DaqoRtcViewFactory類,實現(xiàn)如下三個方法,
    其中主要的是createWithFrame方法提供一個實現(xiàn)了FlutterPlatformView協(xié)議的視圖。
#import <Flutter/Flutter.h>

@interface DaqoRtcViewFactory : NSObject<FlutterPlatformViewFactory>
-(instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end



-(instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
    self = [super init];
    if (self) {
        _messenger = messenger;
    }
    return self;
}

#import "DaqoRtcViewFactory.h"
#import "DaqoPlayerController.h"

@interface DaqoRtcViewFactory ()
@property(nonatomic)NSObject<FlutterBinaryMessenger>* messenger;
@end

@implementation DaqoRtcViewFactory

-(instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
    self = [super init];
    if (self) {
        _messenger = messenger;
    }
    return self;
}

- (NSObject<FlutterMessageCodec>*)createArgsCodec {
    return [FlutterStandardMessageCodec sharedInstance];
}

- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame
                                            viewIdentifier:(int64_t)viewId
                                                 arguments:(id _Nullable)args {
    DaqoPlayerController *controller = [[DaqoPlayerController alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
    return controller;
}

@end

  1. 第三個是實現(xiàn)了FlutterPlatformView協(xié)議的DaqoPlayerController類,該類通過實現(xiàn)協(xié)議的- (nonnull UIView *)view { return _viewLocal;} 方法返回最終提供給flutter的原生視圖。(請注意并不是真的將原生視圖傳到了flutter層,而是將原生視圖數(shù)據(jù)渲染在VirtualDisplay中,返回texureId ,flutter通過這個id獲取到渲染數(shù)據(jù),然后使用skia直接在flutter中渲染,和react-native的插件有本質不同,react-native的插件實際上只是增加了一個新的原生和js組件的映射關系。)

插件的主要代碼都在這里實現(xiàn),還有原生和flutter相互調用也在這里實現(xiàn)。

//
//  DaqoPlayer.m
//  Runner
//
//  Created by merlin song on 2020/6/1.
//  Copyright ? 2020 The Chromium Authors. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AliRTCSdk/AliRtcEngine.h>
#import "DaqoPlayerController.h"
#import "RTCSampleRemoteUserManager.h"
#import "RTCSampleRemoteUserModel.h"

@interface DaqoPlayerController ()<AliRtcEngineDelegate, FlutterStreamHandler>

@property(nonatomic)AliRenderView * viewLocal;
@property(nonatomic)int64_t viewId;
@property(nonatomic)FlutterMethodChannel* channel;
/**
 @brief 是否加入阿里音視頻服務通信頻道
 */
@property(nonatomic, assign) BOOL isJoinChannel;
/**
 @brief SDK實例
 */
@property (nonatomic, strong) AliRtcEngine *engine;
/**
 @brief 遠端用戶管理
 */
@property(nonatomic, strong) RTCSampleRemoteUserManager *remoteUserManager;


@property (nonatomic, strong) FlutterEventSink eventSink;
@property (nonatomic, strong) FlutterEventChannel *eventChannel;
@end

@implementation DaqoPlayerController
{
    AliRtcAuthInfo *authInfo;
    AliVideoCanvas *currentCanvas;
    NSString *currentUid;
    bool isLocalPreview;
}
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger
{
    self = [super init];
    if (self) {
        _viewId = viewId;
        NSString* channelName = [NSString stringWithFormat:@"plugins.daqo_rtc_video_%lld", viewId];
        NSString* eventChannelName = [NSString stringWithFormat:@"plugins.daqo_rtc_event_%lld", viewId];
//flutter調原生的通道
        _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
//原生調用flutter的通道 self.eventSink(@{key: value})
        _eventChannel = [FlutterEventChannel eventChannelWithName:eventChannelName binaryMessenger:messenger];
        [_eventChannel setStreamHandler:self];

        _viewLocal = [[AliRenderView alloc] initWithFrame:frame];

        __weak __typeof__(self) weakSelf = self;
        [_channel setMethodCallHandler:^(FlutterMethodCall *  call, FlutterResult  result) {
            [weakSelf onMethodCall:call result:result];
        }];
    }
    return  self;
}

- (nonnull UIView *)view {
    return _viewLocal;
}


//接收flutter的調用
-(void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
    NSString *key = [call method];
    __weak __typeof__(self) weakSelf = self;
    // dispatch_async(dispatch_get_main_queue(), ^{
        
        if ([key isEqualToString:@"startPreview"]) {
            // 預覽
            [weakSelf startPreview];
        } else if ([key isEqualToString:@"stopPreview"]) {
            // 停止預覽
            [weakSelf stopPreview];
        } else if ([key isEqualToString:@"joinChannel"]) {
            // 加入頻道
            [weakSelf joinChannel:call.arguments result:result];
        }else if ([key isEqualToString:@"showRemoteCamera"]) {
            // 顯示遠端用戶推流
            [weakSelf showRemoteCamera:call.arguments];
        }else if ([key isEqualToString:@"leaveChannel"]) {
            // 顯示遠端用戶推流
            [weakSelf leaveChannel];
        }
        else {
            result(FlutterMethodNotImplemented);
        }
    // });
}

#pragma mark - FlutterStreamHandler
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)eventSink{
    self.eventSink = eventSink;
    return nil;
}
 
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    return nil;
}
@end

  • android
    android端也需要提供類似的三個文件
    實現(xiàn)FlutterPlugin接口的DaqoRTCPlugin類
    實現(xiàn)PlatformViewFactory接口的DaqoRTCViewFactory類
    實現(xiàn)PlatformView接口的DaqoPlayerController類

以上三個類的職責和ios中是一樣的,只是具體要實現(xiàn)的方法有些差別,

DaqoRTCPlugin.java

package com.example.DaqoRTC;

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
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;
import io.flutter.plugin.common.PluginRegistry.Registrar;

/** DaqoRTCPlugin */
public class DaqoRTCPlugin implements FlutterPlugin {
  public static void registerWith(Registrar registrar) {
    registrar
            .platformViewRegistry()
            .registerViewFactory(
                    "plugins.daqo_rtc_video",
                    new DaqoRTCViewFactory(registrar.messenger()));
  }


  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
     BinaryMessenger messenger = flutterPluginBinding.getBinaryMessenger();
     flutterPluginBinding
         .getPlatformViewRegistry()
         .registerViewFactory(
             "plugins.daqo_rtc_video", new DaqoRTCViewFactory(messenger));
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {

  }
}

DaqoRTCViewFactory.java

package com.example.DaqoRTC;

import android.content.Context;
import android.view.View;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import io.flutter.plugin.common.StandardMessageCodec;
import java.util.Map;

public final class DaqoRTCViewFactory extends PlatformViewFactory {
    private final BinaryMessenger messenger;

    DaqoRTCViewFactory(BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
    }

    @SuppressWarnings("unchecked")
    @Override
    public PlatformView create(Context context, int id, Object args) {
        Map<String, Object> params = (Map<String, Object>) args;
        return new DaqoPlayerController(context, messenger, id, params);
    }
}

DaqoPlayerController.java

package com.example.DaqoRTC;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.graphics.Color;

import com.alivc.rtc.AliRtcAuthInfo;
import com.alivc.rtc.AliRtcEngine;
import com.alivc.rtc.AliRtcEngineEventListener;
import com.alivc.rtc.AliRtcEngineNotify;
import com.alivc.rtc.AliRtcRemoteUserInfo;

import org.webrtc.sdk.SophonSurfaceView;

import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
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;
import io.flutter.plugin.platform.PlatformView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import android.widget.Toast;

import static org.webrtc.ali.ThreadUtils.runOnUiThread;

public class DaqoPlayerController implements PlatformView, MethodCallHandler, EventChannel.StreamHandler {

    public EventChannel.EventSink eventSink;
    /**
     * SDK提供的對音視頻通話處理的引擎類
     */
    private AliRtcEngine mEngine;

    // 父容器
//    private LinearLayout mSurfaceContainer;

    // 播放視圖
    private SophonSurfaceView surfaceView;

    // 已訂閱的遠程用戶數(shù)組
    private ArrayList<AliRtcRemoteUserInfo> remoteUsers = new ArrayList<AliRtcRemoteUserInfo>();

    // 本地預覽標志
    boolean isLocalPreview = false;

    boolean isJoinChannel = false;

    // 當前選擇的遠端用戶id
    String currentUid;

    // 當前遠端canvas
    AliRtcEngine.AliVideoCanvas currentCanvas;

    private final MethodChannel methodChannel;

    private  final  EventChannel eventChannel;

    // 用戶信息
    private AliRtcAuthInfo authInfo;

    Context vContext;

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @SuppressWarnings("unchecked")
    DaqoPlayerController(
            final Context context,
            BinaryMessenger messenger,
            int id,
            Map<String, Object> params) {

        // 初始化surface
        vContext = context;
        surfaceView = new SophonSurfaceView(context);
        surfaceView.setZOrderOnTop(true);
        surfaceView.setZOrderMediaOverlay(true);

        // flutter調原生
        methodChannel = new MethodChannel(messenger, "plugins.daqo_rtc_video_" + id);
        methodChannel.setMethodCallHandler(this);

        // 原生調flutter
        eventChannel = new EventChannel(messenger, "plugins.daqo_rtc_event_" + id);
        eventChannel.setStreamHandler(this);
    }


    @Override
    public View getView() {
        return surfaceView;
    }

    @Override
    public void onMethodCall(MethodCall methodCall, Result result) {
        switch (methodCall.method) {
            case "startPreview":
                startPreview();
                break;
            case "stopPreview":
                stopPreview();
                break;
            case "joinChannel":
                joinChannel(methodCall, result);
                break;
            case "leaveChannel":
                leaveChannel();
                break;
            case "showRemoteCamera":
                showRemoteCamera(methodCall, result);
                break;
            default:
                result.notImplemented();
        }
    }

//這個方法非常非常重要,當切換視頻流的時候需要對surfaceView重新布局。
    private void  reLayout() {
        ViewGroup vg = (ViewGroup) surfaceView.getParent();
        vg.removeAllViews();
        vg.addView(surfaceView,
                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
    }


    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
        this.eventSink = events;
    }

    @Override
    public void onCancel(Object arguments) {

    }
}

有關音視頻sdk

音視頻sdk如何初始化,加入頻道訂閱發(fā)布推流等功能文檔中寫的很清楚就不多說了,主要記錄一個浪費了我一天時間的坑,就是我們將一個原生視圖提供給flutter層之后是不能再重建這個視圖的,所以切換推流的時候就不能像文檔中那樣重建視播放視圖了。

查閱文檔中有這么一個方法:
setRemoteViewConfig:為遠端的視頻設置渲染窗口以及繪制參數(shù)。

官方解釋:
支持加入頻道之前和之后切換窗口。如果canvas為NULL或者其成員渲染視圖為NULL,則停止渲染相應的流。
如果在播放過程中需要重新設置渲染方式,請保持canvas中其他成員變量不變,僅修改renderMode。
canvas中渲染方式默認為AliRtcRenderModeAuto。
建議在訂閱結果回調之后調用。

我按照這個方法試了一下在iOS中一切正常,如文檔所說。但是在android中就不行了,怎么都不能停止上一個視頻,因為對android不是很了解,調了好久也沒解決。最后咨詢了技術支持,對方直接專業(yè)的回復了代碼??。 關鍵步驟就是要先將這個公用的播放視圖從父視圖中移除,再添加到父組件上并重新布局。

    private void  reLayout() {
        ViewGroup vg = (ViewGroup) surfaceView.getParent();
        vg.removeAllViews();
        vg.addView(surfaceView,
                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
    }
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容