由于阿里音視頻服務目前僅提供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
工程目錄如下:

其中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中。

3.插件的實現(xiàn)步驟
按照flutter要求提供三個主要文件:
DaqoRTCPlugin
DaqoRtcViewFactory
DaqoPlayerController
- iOS
1.一個實現(xiàn)FlutterPlugin協(xié)議的DaqoRTCPlugin類,實現(xiàn)其registerWithRegistrar靜態(tài)方法,這是插件注冊的入口,app在啟動的時候會調用。據(jù)地調用地方是:


- 第二個是實現(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
- 第三個是實現(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));
}

