由于阿里音视频服务目前仅提供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
工程目录如下:
其中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
在DaqoRTC文件夹下还有个libs文件夹,将三方sdk放在里面,这个文件夹在android studio中看不到没关系。图中com.example.DaqoRTC目录下就是插件java源码。
-
iOS
ios在build的时候会从pod仓库中下载,不用将sdk放到本地。
这样等pod和gradle构建完成之后就可以分别在ios的Classes目录和android的com.example.DaqoRTC目录下愉快的编写插件代码了。然后在各自的工程中直接build测试就行了。
不要忘了在example/lib/mian.dart中添加插件的测试代码。
还有插件的dart代码直接在插件工程的lib目录中添加,注意不是example中。
3.插件的实现步骤
按照flutter要求提供三个主要文件:
DaqoRTCPlugin
DaqoRtcViewFactory
DaqoPlayerController
- iOS
1.一个实现FlutterPlugin协议的DaqoRTCPlugin类,实现其registerWithRegistrar静态方法,这是插件注册的入口,app在启动的时候会调用。据地调用地方是:
- 第二个是实现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
- 第三个是实现了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));
}