Flutter视频会议插件eko_jitsi的使用
Flutter视频会议插件eko_jitsi的使用
Jitsi Meet插件用于Flutter。支持Android和iOS平台。
“Jitsi Meet是一个开源(Apache)WebRTC JavaScript应用程序,使用Jitsi Videobridge来提供高质量、安全且可扩展的视频会议。”
更多信息请访问Jitsi Meet。
目录
配置
iOS
注意:示例在XCode 12.1 & Flutter 1.22.3下编译通过。
Podfile
确保您的Podfile包含如下条目,声明平台为11.0或更高版本。
platform :ios, '11.0'
Info.plist
在Info.plist中添加NSCameraUsageDescription和NSMicrophoneUsageDescription。
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) MyApp需要访问您的摄像头进行会议。</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) MyApp需要访问您的麦克风进行会议。</string>
Android
Gradle
将构建工具gradle的依赖设置为至少3.6.3:
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3' <!-- 升级这个 -->
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
将gradle wrapper设置为至少5.6.4。
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip <!-- 升级这个 -->
通过在您的build.gradle文件中添加以下行来启用Java 1.8兼容性支持:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
AndroidManifest.xml
Jitsi Meet的SDK AndroidManifest.xml将与您的项目冲突,特别是application:label字段。要解决这个问题,进入android/app/src/main/AndroidManifest.xml
并添加工具库和tools:replace="android:label"
到application标签。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="yourpackage.com"
xmlns:tools="http://schemas.android.com/tools">
<application
tools:replace="android:label"
android:name="your.application.name"
android:label="My Application"
android:icon="@mipmap/ic_launcher">
...
</application>
...
</manifest>
最低SDK版本23
在android/app/build.gradle中更新最低sdk版本至23。
defaultConfig {
applicationId "com.ekodemy.eko_jitsi_example"
minSdkVersion 23 //Required for Jitsi
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Proguard
Jitsi的SDK启用了Proguard,但如果没有proguard-rules.pro文件,您的发布apk构建将会缺少Flutter Wrapper和react-native代码。在您的Flutter项目的android/app/build.gradle文件中添加Proguard支持。
buildTypes {
release {
// TODO: 添加自己的签名配置以进行发布构建。
// 目前使用调试密钥,以便`flutter run --release`可以工作。
signingConfig signingConfigs.debug
// 添加以下三行以启用Proguard
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
然后在同一目录下创建一个名为proguard-rules.pro的文件。查看示例应用的proguard-rules.pro文件以了解需要粘贴的内容。
注意
如果您不创建proguard-rules.pro文件,那么当您尝试加入会议或会议屏幕试图打开但立即关闭时,您的应用将会崩溃。您将在logcat中看到以下错误之一。
## 应用崩溃 ##
java.lang.RuntimeException: Parcel android.os.Parcel@8530c57: Unmarshalling unknown type code 7536745 at offset 104
at android.os.Parcel.readValue(Parcel.java:2747)
at android.os.Parcel.readSparseArrayInternal(Parcel.java:3118)
at android.os.Parcel.readSparseArray(Parcel.java:2351)
.....
## 会议无法打开并返回上一个屏幕 ##
W/unknown:ViewManagerPropertyUpdater: Could not find generated setter for class com.BV.LinearGradient.LinearGradientManager
W/unknown:ViewManagerPropertyUpdater: Could not find generated setter for class com.facebook.react.uimanager.g
W/unknown:ViewManagerPropertyUpdater: Could not find generated setter for class com.facebook.react.views.art.ARTGroupViewManager
W/unknown:ViewManagerPropertyUpdater: Could not find generated setter for class com.facebook.react.views.art.a
.....
加入会议
_joinMeeting() async {
try {
var options = JitsiMeetingOptions()
..room = "myroom" // 必须,空格会被删除
..serverURL = "https://someHost.com"
..subject = "Meeting with Shubham"
..userDisplayName = "My Name"
..userEmail = "myemail@email.com"
..audioOnly = true
..audioMuted = true
..videoMuted = true;
await EkoJitsi.joinMeeting(options);
} catch (error) {
debugPrint("error: $error");
}
}
JitsiMeetingOptions
字段 | 是否必须 | 默认值 | 描述 |
---|---|---|---|
room | 是 | N/A | 唯一的房间名,附加到serverURL。有效字符:字母数字、破折号和下划线。 |
subject | 否 | $room | 会议名称显示在会议顶部。如果为空,则默认为房间名,破折号和下划线替换为空格,首字母大写。 |
userDisplayName | 否 | “Fellow Jitster” | 用户的显示名称 |
userEmail | 否 | none | 用户的电子邮件地址 |
audioOnly | 否 | false | 无视频开始会议。可以在会议中切换。 |
audioMuted | 否 | false | 开始会议时静音。可以在会议中切换。 |
videoMuted | 否 | false | 开始会议时关闭视频。可以在会议中切换。 |
serverURL | 否 | meet.jitsi.si | 指定您自己的托管服务器。必须是格式为<scheme>://<host>[/path] 的有效绝对URL,例如https://someHost.com 。默认为Jitsi Meet的服务器。 |
userAvatarURL | N/A | none | 尚未实现。用户的头像URL。 |
token | N/A | none | 用于身份验证的JWT令牌。 |
featureFlags | 否 | 见下文 | 用于启用/禁用Jitsi Meet SDK的功能的特征标志映射 |
FeatureFlags
特征标志允许您启用或禁用Jitsi Meet SDK的任何功能。
如果您未向JitsiMeetingOptions提供任何标志,默认值将被使用。
如果您未向JitsiMeetingOptions的featureFlags提供标志,则其默认值将被使用。
我们使用的是来自Jitsi Meet仓库的官方标志列表。
标志 | 默认值 (Android) | 默认值 (iOS) | 描述 |
---|---|---|---|
ADD_PEOPLE_ENABLED | true | true | 启用蓝色按钮"Add people",当您独自一人时会显示出来。需要标志INVITE_ENABLED才能工作。 |
CALENDAR_ENABLED | true | auto | 启用日历集成。 |
CALL_INTEGRATION_ENABLED | true | true | 启用呼叫集成(iOS上的CallKit,Android上的ConnectionService)。见备注 |
CLOSE_CAPTIONS_ENABLED | true | true | 启用菜单中的字幕选项。 |
CHAT_ENABLED | true | true | 启用聊天(按钮和功能)。 |
INVITE_ENABLED | true | true | 启用菜单中的邀请选项。 |
IOS_RECORDING_ENABLED | N/A | false | 在iOS上启用录制。 |
LIVE_STREAMING_ENABLED | auto | auto | 启用菜单中的直播选项。 |
MEETING_NAME_ENABLED | true | true | 显示会议名称。 |
MEETING_PASSWORD_ENABLED | true | true | 启用菜单中的会议密码选项(如果设置了会议密码,对话框仍然会出现)。 |
PIP_ENABLED | auto | auto | 启用画中画模式。 |
RAISE_HAND_ENABLED | true | true | 启用菜单中的举手选项。 |
RECORDING_ENABLED | auto | N/A | 启用菜单中的录制选项。 |
TILE_VIEW_ENABLED | true | true | 启用菜单中的拼图视图选项。 |
TOOLBOX_ALWAYS_VISIBLE | true | true | 工具栏(按钮和菜单)始终在通话期间可见(如果不这样,单击即可显示)。 |
WELCOME_PAGE_ENABLED | false | false | 启用欢迎页面。“欢迎页面列出了最近的会议和日历约会,旨在用于独立应用程序。” |
关于呼叫集成的备注 呼叫集成在Android(称为ConnectionService)已被官方Jitsi Meet应用禁用,因为它引起了很多问题。您应该也禁用它以避免这些问题。
JitsiMeetingResponse
字段 | 类型 | 描述 |
---|---|---|
isSuccess | bool | 成功指示器。 |
message | String | 成功消息或错误作为字符串。 |
error | dynamic | 可选,仅在isSuccess为false时存在。错误对象。 |
监听会议事件
支持的事件
名称 | 描述 |
---|---|
onConferenceWillJoin | 会议正在加载。 |
onConferenceJoined | 用户已加入会议。 |
onConferenceTerminated | 用户已退出会议。 |
onError | 监听会议事件时发生错误。 |
每个会议的事件
要为每个会议监听事件,在joinMeeting中传递一个JitsiMeetingListener。当触发onConferenceTerminated事件时,监听器将自动移除。
await EkoJitsi.joinMeeting(options,
listener: EkoJitsiListener(onConferenceWillJoin: ({message}) {
debugPrint("${options.room} will join with message: $message");
}, onConferenceJoined: ({message}) {
debugPrint("${options.room} joined with message: $message");
}, onConferenceTerminated: ({message}) {
debugPrint("${options.room} terminated with message: $message");
}));
全局会议事件
要监听全局会议事件,只需添加一个JitsiMeetListener,并使用JitsiMeet.addListener(myListener)
。您可以使用JitsiMeet.removeListener(listener)
或JitsiMeet.removeAllListeners()
来移除监听器。
[@override](/user/override)
void initState() {
super.initState();
JitsiMeet.addListener(JitsiMeetingListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onError: _onError));
}
[@override](/user/override)
void dispose() {
super.dispose();
JitsiMeet.removeAllListeners();
}
_onConferenceWillJoin({message}) {
debugPrint("_onConferenceWillJoin broadcasted");
}
_onConferenceJoined({message}) {
debugPrint("_onConferenceJoined broadcasted");
}
_onConferenceTerminated({message}) {
debugPrint("_onConferenceTerminated broadcasted");
}
_onError(error) {
debugPrint("_onError broadcasted");
}
程序化关闭会议
EkoJitsi.closeMeeting();
贡献
发送包含尽可能多信息的拉取请求,清楚地描述问题或功能。保持更改小,一次只针对一个问题。
功能请求
首先,此插件使用Jitsi Meet的移动SDK,因此如果他们的SDK不支持某个功能,此插件可能也无法做到。检查Jitsi Meet是否支持您的请求。如果不支持,请向Jitsi Meet团队提交功能请求而不是在这里提交。
如果新功能在Jitsi Meet的SDK中可用,但在本插件中不可用,或者新功能与SDK无关,请使用以下模板在这里提交请求:
新功能请求
用例:从用户或开发者的角度来看,描述功能请求的用例。例如,“作为一个用户,我希望可以通过语音命令关闭会议。” 或 “作为一个开发者,我希望检测用户眨眼。” 包含尽可能多的细节。
包括示例,如截图、UX设计、故事板、其他应用或代码,展示该功能的样子。
问题
在此处打开问题:在这里。使用以下模板:
平台:指定一个或多个:[iOS, Android]
设备物理或模拟器:[Physical, Simulator]
设备型号:指定型号
设备操作系统版本:指定Android或iOS版本
Flutter Doctor:运行flutter doctor并粘贴结果:
--- flutter doctor 结果在这里 ---
重现步骤:详细步骤如下:
1. 步骤1
2. 步骤2
...
错误日志:
--- 如果有任何,请粘贴错误日志 ---
完整示例代码
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:eko_jitsi/feature_flag/feature_flag_enum.dart';
import 'package:eko_jitsi/eko_jitsi.dart';
import 'package:eko_jitsi/eko_jitsi_listener.dart';
import 'package:eko_jitsi/room_name_constraint.dart';
import 'package:eko_jitsi/room_name_constraint_type.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
[@override](/user/override)
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final serverText = TextEditingController();
final roomText = TextEditingController(text: "plugintestroom");
final subjectText = TextEditingController(text: "My Plugin Test Meeting");
final nameText = TextEditingController(text: "Plugin Test User");
final emailText = TextEditingController(text: "fake@email.com");
var isAudioOnly = true;
var isAudioMuted = true;
var isVideoMuted = true;
[@override](/user/override)
void initState() {
super.initState();
EkoJitsi.addListener(EkoJitsiListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onError: _onError));
}
[@override](/user/override)
void dispose() {
super.dispose();
EkoJitsi.removeAllListeners();
}
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('插件示例应用'),
),
body: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(
height: 24.0,
),
TextField(
controller: serverText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "服务器URL",
hintText: "提示:留空使用meet.jitsi.si"),
),
SizedBox(
height: 16.0,
),
TextField(
controller: roomText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "房间",
),
),
SizedBox(
height: 16.0,
),
TextField(
controller: subjectText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "主题",
),
),
SizedBox(
height: 16.0,
),
TextField(
controller: nameText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "显示名称",
),
),
SizedBox(
height: 16.0,
),
TextField(
controller: emailText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "电子邮件",
),
),
SizedBox(
height: 16.0,
),
SizedBox(
height: 16.0,
),
CheckboxListTile(
title: Text("仅音频"),
value: isAudioOnly,
onChanged: _onAudioOnlyChanged,
),
SizedBox(
height: 16.0,
),
CheckboxListTile(
title: Text("音频静音"),
value: isAudioMuted,
onChanged: _onAudioMutedChanged,
),
SizedBox(
height: 16.0,
),
CheckboxListTile(
title: Text("视频静音"),
value: isVideoMuted,
onChanged: _onVideoMutedChanged,
),
Divider(
height: 48.0,
thickness: 2.0,
),
SizedBox(
height: 64.0,
width: double.maxFinite,
child: ElevatedButton(
onPressed: () {
_joinMeeting();
},
child: Text(
"加入会议",
style: TextStyle(color: Colors.white),
),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.blue))),
),
SizedBox(
height: 48.0,
),
],
),
),
),
),
);
}
_onAudioOnlyChanged(bool value) {
setState(() {
isAudioOnly = value;
});
}
_onAudioMutedChanged(bool value) {
setState(() {
isAudioMuted = value;
});
}
_onVideoMutedChanged(bool value) {
setState(() {
isVideoMuted = value;
});
}
_joinMeeting() async {
String serverUrl =
serverText.text?.trim()?.isEmpty ?? "" ? null : serverText.text;
try {
// 启用或禁用任何功能标志
// 如果未提供功能标志,将使用默认值
// 功能标志的完整列表及其默认值请参阅README
Map<FeatureFlagEnum, bool> featureFlags = {
FeatureFlagEnum.WELCOME_PAGE_ENABLED: false,
};
// 这里是一个例子,为每个平台禁用功能
if (Platform.isAndroid) {
// 禁用Android上的ConnectionService使用以避免问题(参见README)
featureFlags[FeatureFlagEnum.CALL_INTEGRATION_ENABLED] = false;
} else if (Platform.isIOS) {
// 禁用iOS上的画中画,因为看起来很奇怪
featureFlags[FeatureFlagEnum.PIP_ENABLED] = false;
}
// 定义会议选项
var options = JitsiMeetingOptions()
..room = roomText.text
..serverURL = serverUrl
..subject = subjectText.text
..userDisplayName = nameText.text
..userEmail = emailText.text
..audioOnly = isAudioOnly
..audioMuted = isAudioMuted
..videoMuted = isVideoMuted
..featureFlags.addAll(featureFlags);
debugPrint("JitsiMeetingOptions: $options");
await EkoJitsi.joinMeeting(
options,
listener: EkoJitsiListener(onConferenceWillJoin: ({message}) {
debugPrint("${options.room} 将加入会议,消息:$message");
}, onConferenceJoined: ({message}) {
debugPrint("${options.room} 已加入会议,消息:$message");
}, onConferenceTerminated: ({message}) {
debugPrint("${options.room} 已终止会议,消息:$message");
}),
// 默认情况下,使用插件默认约束
//roomNameConstraints: new Map(), // 禁用所有约束
//roomNameConstraints: customContraints, // 使用自定义约束
);
} catch (error) {
debugPrint("error: $error");
}
}
static final Map<RoomNameConstraintType, RoomNameConstraint>
customContraints = {
RoomNameConstraintType.MAX_LENGTH: new RoomNameConstraint((value) {
return value.trim().length <= 50;
}, "最大房间名长度应为50。"),
RoomNameConstraintType.FORBIDDEN_CHARS: new RoomNameConstraint((value) {
return RegExp(r"[$€£]+", caseSensitive: false, multiLine: false)
.hasMatch(value) ==
false;
}, "不允许在房间名中使用货币符号。"),
};
void _onConferenceWillJoin({message}) {
debugPrint("_onConferenceWillJoin 广播了,消息:$message");
}
void _onConferenceJoined({message}) {
debugPrint("_onConferenceJoined 广播了,消息:$message");
}
void _onConferenceTerminated({message}) {
debugPrint("_onConferenceTerminated 广播了,消息:$message");
}
_onError(error) {
debugPrint("_onError 广播了,错误:$error");
}
}
更多关于Flutter视频会议插件eko_jitsi的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html