flutter插件封装阿里音视频服务sdk

由于阿里音视频服务目前仅提供android和ios的原生sdk,但我们的项目准备用flutter。so需要封装一个flutter插件, 这里记录下封装过程,方便以后快速回忆。

0.基本需求
  • 提供基础音视频通话画面。
  • 提供本地摄像头预览功能
  • 能够加入退出频道,发布本地视频推流,和订阅远端其他用户视频流。
  • 能够在所有订阅用户和本地视频流之间进行画面切换。
  • 扩展 其他业务相关功能在flutter层做。

通过sdk文档发现只需要将用于播放视频的视图提供到flutter即可,ios是AliRenderView,android端是SophonSurfaceView。

1.使用工具

VSCode, Xcode, Android Studio

VSCode用于编辑插件工程,主要是编写flutter端插件代码以及测试demo代码。
Xcode和Android Studio分别用来编辑对应平台的原生插件代买,为了利用其代码检查,与调试。

2.创建插件工程

貌似默认语言是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中看不到没关系。图中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.插件的实现步骤

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

  • iOS

1.一个实现FlutterPlugin协议的DaqoRTCPlugin类,实现其registerWithRegistrar静态方法,这是插件注册的入口,app在启动的时候会调用。据地调用地方是:


EA70D678-953B-4965-9EA7-A92BBE8352C4.png
9708074E-2A91-4A1D-8877-876034923282.png
  1. 第二个是实现FlutterPlatformViewFactory协议的DaqoRtcViewFactory类,实现如下三个方法,
    其中主要的是createWithFrame方法提供一个实现了FlutterPlatformView协议的视图。
#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. 第三个是实现了FlutterPlatformView协议的DaqoPlayerController类,该类通过实现协议的- (nonnull UIView *)view { return _viewLocal;} 方法返回最终提供给flutter的原生视图。(请注意并不是真的将原生视图传到了flutter层,而是将原生视图数据渲染在VirtualDisplay中,返回texureId ,flutter通过这个id获取到渲染数据,然后使用skia直接在flutter中渲染,和react-native的插件有本质不同,react-native的插件实际上只是增加了一个新的原生和js组件的映射关系。)

插件的主要代码都在这里实现,还有原生和flutter相互调用也在这里实现。

//
//  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端也需要提供类似的三个文件
    实现FlutterPlugin接口的DaqoRTCPlugin类
    实现PlatformViewFactory接口的DaqoRTCViewFactory类
    实现PlatformView接口的DaqoPlayerController类

以上三个类的职责和ios中是一样的,只是具体要实现的方法有些差别,

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;

    // 已订阅的远程用户数组
    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如何初始化,加入频道订阅发布推流等功能文档中写的很清楚就不多说了,主要记录一个浪费了我一天时间的坑,就是我们将一个原生视图提供给flutter层之后是不能再重建这个视图的,所以切换推流的时候就不能像文档中那样重建视播放视图了。

查阅文档中有这么一个方法:
setRemoteViewConfig:为远端的视频设置渲染窗口以及绘制参数。

官方解释:
支持加入频道之前和之后切换窗口。如果canvas为NULL或者其成员渲染视图为NULL,则停止渲染相应的流。
如果在播放过程中需要重新设置渲染方式,请保持canvas中其他成员变量不变,仅修改renderMode。
canvas中渲染方式默认为AliRtcRenderModeAuto。
建议在订阅结果回调之后调用。

我按照这个方法试了一下在iOS中一切正常,如文档所说。但是在android中就不行了,怎么都不能停止上一个视频,因为对android不是很了解,调了好久也没解决。最后咨询了技术支持,对方直接专业的回复了代码😶。 关键步骤就是要先将这个公用的播放视图从父视图中移除,再添加到父组件上并重新布局。

    private void  reLayout() {
        ViewGroup vg = (ViewGroup) surfaceView.getParent();
        vg.removeAllViews();
        vg.addView(surfaceView,
                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342