FlutterFlutter

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

2020-06-12  本文已影响0人  merlinCry

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

0.基本需求

通过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配置
配置方法如下:

在DaqoRTC文件夹下还有个libs文件夹,将三方sdk放在里面,这个文件夹在android studio中看不到没关系。图中com.example.DaqoRTC目录下就是插件java源码。

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

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

以上三个类的职责和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));
    }
上一篇 下一篇

猜你喜欢

热点阅读