Flutter视频会议插件jitsi_meeting_plus的使用
Flutter视频会议插件jitsi_meeting_plus的使用
此插件是从以下地址克隆的: https://pub.dev/packages/jitsi_meet
Jitsi Meet插件用于Flutter。支持Android和iOS平台。
“Jitsi Meet是一款开源(Apache)WebRTC JavaScript应用程序,使用Jitsi Videobridge来提供高质量、安全且可扩展的视频会议。”
更多信息,请访问: https://github.com/jitsi/jitsi-meet
目录
配置
iOS
注意:示例可在XCode 12.2 和 Flutter 1.22.4下编译。
Podfile
确保在Podfile中包含以下内容,指定平台版本为11.0或以上:
platform :ios, '11.0'
Info.plist
添加NSCameraUsageDescription和NSMicrophoneUsageDescription到Info.plist文件中:
<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.gunschu.jitsi_meet_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 {
FeatureFlag featureFlag = FeatureFlag();
featureFlag.welcomePageEnabled = false;
featureFlag.resolution = FeatureFlagVideoResolution.MD_RESOLUTION; // 限制视频分辨率为360p
var options = JitsiMeetingOptions()
..room = "myroom" // 必填,空格会被删除
..serverURL = "https://someHost.com"
..subject = "Meeting with Gunschu"
..userDisplayName = "My Name"
..userEmail = "myemail@email.com"
..userAvatarURL = "https://someimageurl.com/image.jpg" // 或.png
..audioOnly = true
..audioMuted = true
..videoMuted = true
..featureFlag = featureFlag;
await JitsiMeet.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 | 指定自己的托管服务器。必须是格式正确的绝对URL,如 https://someHost.com。默认为Jitsi Meet的服务器。 |
userAvatarURL | N/A | none | 用户的头像URL。 |
token | N/A | none | 用于认证的JWT令牌。 |
featureFlag | 否 | 见下文 | FeatureFlag类的对象,用于启用/禁用功能并设置Jitsi Meet SDK的视频分辨率。 |
FeatureFlag
特性标志允许您限制视频分辨率并启用/禁用Jitsi Meet SDK中提到的一些功能。如果您未向JitsiMeetingOptions提供任何标志,则将使用默认值。
我们正在使用来自Jitsi Meet存储库的官方标志列表: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
标志 | 默认 (Android) | 默认 (iOS) | 描述 |
---|---|---|---|
addPeopleEnabled | true | true | 启用蓝色按钮"Add people",当您单独通话时出现。需要启用邀请选项。 |
calendarEnabled | true | auto | 启用日历集成。 |
callIntegrationEnabled | true | true | 启用呼叫集成(iOS上的CallKit,Android上的ConnectionService)。 见下文 |
closeCaptionsEnabled | true | true | 启用菜单中的字幕选项。 |
conferenceTimerEnabled | true | true | 启用会议计时器。 |
chatEnabled | true | true | 启用聊天(按钮和功能)。 |
inviteEnabled | true | true | 启用菜单中的邀请选项。 |
iOSRecordingEnabled | N/A | false | 在iOS上启用录制。 |
kickOutEnabled | true | true | 启用视频缩略图中的踢出选项。 |
liveStreamingEnabled | auto | auto | 启用菜单中的直播选项。 |
meetingNameEnabled | true | true | 显示会议名称。 |
meetingPasswordEnabled | true | true | 启用菜单中的会议密码选项(如果设置了会议密码,对话框仍会显示)。 |
pipEnabled | auto | auto | 启用画中画模式。 |
raiseHandEnabled | true | true | 启用菜单中的举手选项。 |
recordingEnabled | auto | N/A | 启用菜单中的录制选项。 |
resoulution | N/A | N/A | 设置本地和(最大)远程视频分辨率。覆盖服务器配置。接受的值有:LD_RESOLUTION(180p),MD_RESOLUTION(360p),SD_RESOLUTION(480p(SD)),HD_RESOLUTION(720p(HD))。 |
serverURLChangeEnabled | true | true | 启用服务器URL更改。 |
tileViewEnabled | true | true | 启用菜单中的平铺视图选项。 |
toolboxAlwaysVisible | true | true | 工具箱(按钮和菜单)在通话期间始终可见(如果不是,则单击一次即可显示)。 |
videoShareButtonEnabled | true | true | 启用视频共享按钮。 |
welcomePageEnabled | false | false | 启用欢迎页面。“欢迎页面列出了最近的会议和日历预约,旨在被独立应用程序使用。” |
关于呼叫集成的说明
Android上的呼叫集成(称为ConnectionService)由于造成许多问题,已在官方Jitsi Meet应用中禁用。你也应该禁用它以避免这些问题。
JitsiMeetingResponse
字段 | 类型 | 描述 |
---|---|---|
isSuccess | bool | 成功指示器。 |
message | String | 成功消息或错误。 |
error | dynamic | 可选,仅在isSuccess为false时存在。错误对象。 |
监听会议事件
支持的事件
名称 | 描述 |
---|---|
onConferenceWillJoin | 会议正在加载。 |
onConferenceJoined | 用户已加入会议。 |
onConferenceTerminated | 用户已退出会议。 |
onPictureInPictureWillEnter | 用户进入画中画模式。 |
onPictureInPictureTerminated | 用户退出画中画模式。 |
onError | 监听会议事件时发生错误。 |
每次会议事件
要按每次会议监听事件,请在joinMeeting
中传递一个JitsiMeetingListener
。当onConferenceTerminated
事件触发时,监听器将自动移除。
await JitsiMeet.joinMeeting(options,
listener: JitsiMeetingListener(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");
}, onPictureInPictureWillEnter: ({message}) {
debugPrint("${options.room} entered PIP mode with message: $message");
}, onPictureInPictureTerminated: ({message}) {
debugPrint("${options.room} exited PIP mode with message: $message");
}));
全局会议事件
要监听全局会议事件,请简单地使用JitsiMeet.addListener(myListener)
。可以使用JitsiMeet.removeListener(listener)
或JitsiMeet.removeAllListeners()
来移除监听器。
[@override](/user/override)
void initState() {
super.initState();
JitsiMeet.addListener(JitsiMeetingListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onPictureInPictureWillEnter: _onPictureInPictureWillEnter,
onPictureInPictureTerminated: _onPictureInPictureTerminated,
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");
}
_onPictureInPictureWillEnter({message}) {
debugPrint("_onPictureInPictureWillEnter broadcasted with message: $message");
}
_onPictureInPictureTerminated({message}) {
debugPrint("_onPictureInPictureTerminated broadcasted with message: $message");
}
_onError(error) {
debugPrint("_onError broadcasted");
}
程序化关闭会议
JitsiMeet.closeMeeting();
贡献
发送带有尽可能多信息的拉取请求,清楚地描述问题或功能。保持更改小,并针对一个问题一次进行。
功能请求
首先,此插件使用了Jitsi Meet的移动SDK,因此如果他们的SDK不支持某个功能,此插件可能也无法实现。检查Jitsi Meet是否支持您的请求。如果不支持,请向Jitsi Meet团队而不是这里提出功能请求。
如果新功能在Jitsi Meet的SDK中可用,但在本插件中不可用,或者新功能与SDK无关,请使用以下模板提交请求:
New Feature Request
使用场景:从用户或开发者的角度描述功能请求的使用场景。例如,“作为用户,我希望可以通过语音命令关闭会议。” 或 “作为开发者,我希望检测到用户眨眼。” 包括尽可能多的细节。
包括示例,如截图、用户体验设计、故事板或其他应用或代码示例,展示该功能的样子。
问题
在此处打开问题: https://github.com/gunschu/jitsi_meet/issues
使用以下模板:
平台:指定一个或两个:[iOS, Android]
设备物理或模拟器:[Physical, Simulator]
设备型号:指定型号
设备操作系统版本:指定Android或iOS版本
Flutter Doctor:运行flutter doctor并粘贴结果:
--- flutter doctor 结果在这里 ---
重现步骤:详细分步说明如何重现:
1. 步骤1
2. 步骤2
...
错误日志:
--- 如果有任何,请粘贴错误日志 ---
示例代码
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jitsi_meeting_plus/jitsi_meet_plus.dart';
// import 'package:jitsi_meet/jitsi_meet.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: Meeting());
}
}
class Meeting extends StatefulWidget {
[@override](/user/override)
_MeetingState createState() => _MeetingState();
}
class _MeetingState extends State<Meeting> {
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");
final iosAppBarRGBAColor =
TextEditingController(text: "#0080FF80"); //透明蓝
bool? isAudioOnly = true;
bool? isAudioMuted = true;
bool? isVideoMuted = true;
[@override](/user/override)
void initState() {
super.initState();
JitsiMeetPlus.addListener(JitsiMeetingListener(
// JitsiMeet.addListener(JitsiMeetingListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onPictureInPictureWillEnter: _onPictureInPictureWillEnter,
onPictureInPictureTerminated: _onPictureInPictureTerminated,
onError: _onError));
}
[@override](/user/override)
void dispose() {
super.dispose();
JitsiMeetPlus.removeAllListeners();
// JitsiMeet.removeAllListeners();
}
[@override](/user/override)
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('插件示例应用'),
),
body: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: kIsWeb
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: width * 0.30,
child: meetConfig(),
),
Container(
width: width * 0.60,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
color: Colors.white54,
child: SizedBox(
width: width * 0.60 * 0.70,
height: width * 0.60 * 0.70,
child: JitsiMeetConferencing(
extraJS: [
// extraJs setup example
'<script>function echo(){console.log("echo!!!")};</script>',
'<script src="https://code.jquery.com/jquery-3.5.1.slim.js" integrity="sha256-DrT5NfxfbHvMHux31Lkhxg42LY6of8TaYyK50jnxRnM=" crossorigin="anonymous"></script>'
],
),
)),
))
],
)
: meetConfig(),
),
),
);
}
Widget meetConfig() {
return SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(
height: 16.0,
),
TextField(
controller: serverText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "服务器URL",
hintText: "提示:留空则使用meet.jitsi.si"),
),
SizedBox(
height: 14.0,
),
TextField(
controller: roomText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "房间",
),
),
SizedBox(
height: 14.0,
),
TextField(
controller: subjectText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "主题",
),
),
SizedBox(
height: 14.0,
),
TextField(
controller: nameText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "显示名称",
),
),
SizedBox(
height: 14.0,
),
TextField(
controller: emailText,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "电子邮件",
),
),
SizedBox(
height: 14.0,
),
TextField(
controller: iosAppBarRGBAColor,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "AppBar颜色(仅限iOS)",
hintText: "提示:必须以十六进制RGBA格式输入"),
),
SizedBox(
height: 14.0,
),
CheckboxListTile(
title: Text("仅音频"),
value: isAudioOnly,
onChanged: _onAudioOnlyChanged,
),
SizedBox(
height: 14.0,
),
CheckboxListTile(
title: Text("音频静音"),
value: isAudioMuted,
onChanged: _onAudioMutedChanged,
),
SizedBox(
height: 14.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:
MaterialStateColor.resolveWith((states) => 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;
// 启用或禁用任何功能标志
// 如果未提供功能标志,将使用默认值
// 完整的功能标志列表(及其默认值)可在README中找到
Map<FeatureFlagEnum, bool> featureFlags = {
FeatureFlagEnum.WELCOME_PAGE_ENABLED: false,
FeatureFlagEnum.OVERFLOW_MENU_ENABLED: false,
FeatureFlagEnum.PIP_ENABLED: true,
};
if (!kIsWeb) {
// 这里是一个示例,为每个平台禁用功能
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
..iosAppBarRGBAColor = iosAppBarRGBAColor.text
..audioOnly = isAudioOnly
..audioMuted = isAudioMuted
..videoMuted = isVideoMuted
..featureFlags.addAll(featureFlags)
..webOptions = {
"roomName": roomText.text,
"width": "100%",
"height": "100%",
"enableWelcomePage": false,
"chromeExtensionBanner": null,
"userInfo": {"displayName": nameText.text}
};
debugPrint("JitsiMeetingOptions: $options");
await JitsiMeetPlus.joinMeeting(
// await JitsiMeet.joinMeeting(
options,
listener: JitsiMeetingListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onPictureInPictureWillEnter: _onPictureInPictureWillEnter,
onPictureInPictureTerminated: _onPictureInPictureTerminated,
genericListeners: [
JitsiGenericListener(
eventName: 'readyToClose',
callback: (dynamic message) {
debugPrint("readyToClose callback");
}),
]),
);
}
void _onConferenceWillJoin(message) {
debugPrint("_onConferenceWillJoin broadcasted with message: $message");
}
void _onConferenceJoined(message) {
debugPrint("_onConferenceJoined broadcasted with message: $message");
}
void _onConferenceTerminated(message) {
debugPrint("_onConferenceTerminated broadcasted with message: $message");
}
_onError(error) {
debugPrint("_onError broadcasted: $error");
}
_onPictureInPictureWillEnter(Map message) {
debugPrint(
"_onPictureInPictureWillEnter broadcasted with message: $message");
}
_onPictureInPictureTerminated(Map message) {
debugPrint(
"_onPictureInPictureTerminated broadcasted with message: $message");
}
}
更多关于Flutter视频会议插件jitsi_meeting_plus的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html