Flutter视频会议插件bspoke_jitsi_meet的使用
Flutter视频会议插件bspoke_jitsi_meet的使用
本文档将介绍如何在Flutter项目中使用bspoke_jitsi_meet
插件来实现视频会议功能。该插件支持Android、iOS和Web平台,并基于开源的Jitsi Meet项目。
配置
iOS
Podfile
确保在你的Podfile
中添加以下内容,声明最低平台版本为11.0,并禁用BITCODE。
platform :ios, '11.0'
...
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
Info.plist
在Info.plist
文件中添加NSCameraUsageDescription
和NSMicrophoneUsageDescription
字段。
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) MyApp需要访问您的摄像头进行会议。</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) MyApp需要访问您的麦克风进行会议。</string>
Android
Gradle
设置build.gradle
文件中的构建工具版本为至少3.6.3,同时设置Gradle Wrapper版本为至少5.6.4。
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3' <!-- 升级此版本 -->
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
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 <!-- 升级此版本 -->
AndroidManifest.xml
解决Jitsi Meet SDK的application:label
字段与项目冲突问题,在AndroidManifest.xml
中添加tools:replace="android:label"
。
<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
启用ProGuard并创建proguard-rules.pro
文件以避免崩溃问题。
在android/app/build.gradle
中添加以下配置:
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
然后在相同目录下创建proguard-rules.pro
文件,内容可以参考官方示例。
Web
在web/index.html
中引入Jitsi的JavaScript库。
<script src="https://meet.jit.si/external_api.js" type="application/javascript"></script>
加入会议
通过JitsiMeetingOptions
定义会议参数并调用JitsiMeet.joinMeeting
方法加入会议。
_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格式。 |
userAvatarURL | 否 | none | 用户头像URL。 |
token | 否 | none | JWT令牌用于身份验证。 |
featureFlag | 否 | 见下方描述 | 使用FeatureFlag 类对象控制功能开关及视频分辨率。 |
FeatureFlag
FeatureFlag
允许限制视频分辨率并启用/禁用某些功能。如果不提供任何标志,则使用默认值。
标志 | 默认(Android) | 默认(iOS) | 描述 |
---|---|---|---|
addPeopleEnabled | true | true | 启用“添加人员”按钮。 |
calendarEnabled | true | auto | 启用日历集成。 |
callIntegrationEnabled | true | true | 启用通话集成(iOS的CallKit,Android的ConnectionService)。 |
closeCaptionsEnabled | true | true | 启用字幕选项。 |
… | … | … | … |
JitsiMeetingResponse
字段 | 类型 | 描述 |
---|---|---|
isSuccess | bool | 成功指示符。 |
message | String | 成功消息或错误信息。 |
error | dynamic | 可选,仅当isSuccess 为false时存在,错误对象。 |
监听会议事件
可以通过监听特定会议事件或全局事件来获取会议状态变化。
每次会议事件
await JitsiMeet.joinMeeting(options,
listener: JitsiMeetingListener(
onConferenceWillJoin: ({message}) {
debugPrint("${options.room} 将加入会议,消息:$message");
},
onConferenceJoined: ({message}) {
debugPrint("${options.room} 已加入会议,消息:$message");
},
onConferenceTerminated: ({message}) {
debugPrint("${options.room} 已退出会议,消息:$message");
},
onPictureInPictureWillEnter: ({message}) {
debugPrint("${options.room} 进入PIP模式,消息:$message");
},
onPictureInPictureTerminated: ({message}) {
debugPrint("${options.room} 退出PIP模式,消息:$message");
},
onError: (error) {
debugPrint("发生错误:$error");
},
));
全局会议事件
[@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广播");
}
_onConferenceJoined({message}) {
debugPrint("_onConferenceJoined广播");
}
_onConferenceTerminated({message}) {
debugPrint("_onConferenceTerminated广播");
}
_onPictureInPictureWillEnter({message}) {
debugPrint("_onPictureInPictureWillEnter广播,消息:$message");
}
_onPictureInPictureTerminated({message}) {
debugPrint("_onPictureInPictureTerminated广播,消息:$message");
}
_onError(error) {
debugPrint("_onError广播,错误:$error");
}
程序化结束会议
通过以下代码结束当前会议:
JitsiMeet.closeMeeting();
完整示例代码
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:bspoke_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();
JitsiMeet.addListener(JitsiMeetingListener(
onConferenceWillJoin: _onConferenceWillJoin,
onConferenceJoined: _onConferenceJoined,
onConferenceTerminated: _onConferenceTerminated,
onError: _onError));
}
[@override](/user/override)
void dispose() {
super.dispose();
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: [
'<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: "提示:必须为HEX 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;
Map<FeatureFlagEnum, bool> featureFlags = {
FeatureFlagEnum.WELCOME_PAGE_ENABLED: false,
FeatureFlagEnum.INVITE_ENABLED: true
};
if (!kIsWeb) {
if (Platform.isAndroid) {
featureFlags[FeatureFlagEnum.CALL_INTEGRATION_ENABLED] = false;
} else if (Platform.isIOS) {
featureFlags[FeatureFlagEnum.PIP_ENABLED] = false;
}
}
var options = JitsiMeetingOptions(room: roomText.text)
..serverURL = 'https://meeting.vhglobal.org'
..subject = subjectText.text
..userDisplayName = nameText.text
..userEmail = emailText.text
..iosAppBarRGBAColor = iosAppBarRGBAColor.text
..audioOnly = false
..audioMuted = false
..videoMuted = false
..featureFlags.addAll(featureFlags)
..webOptions = {
"roomName": roomText.text,
"width": "100%",
"height": "100%",
"enableWelcomePage": false,
"chromeExtensionBanner": null,
"userInfo": {"displayName": nameText.text}
};
print("JitsiMeetingOptions: $options");
await JitsiMeet.joinMeeting(
options,
listener: JitsiMeetingListener(
onConferenceWillJoin: (message) {
print("${options.room} 将加入会议,消息:$message");
},
onConferenceJoined: (message) {
print("${options.room} 已加入会议,消息:$message");
},
onConferenceTerminated: (message) {
print("${options.room} 已退出会议,消息:$message");
},
onError: (message) {
print("错误消息:$message");
},
genericListeners: [
JitsiGenericListener(
eventName: 'readyToClose',
callback: (dynamic message) {
print("readyToClose回调");
}),
]),
);
}
void _onConferenceWillJoin(message) {
print("_onConferenceWillJoin广播,消息:$message");
}
void _onConferenceJoined(message) {
print("_onConferenceJoined广播,消息:$message");
}
void _onConferenceTerminated(message) {
print("_onConferenceTerminated广播,消息:$message");
}
_onError(error) {
print("_onError广播,错误:$error");
}
}
更多关于Flutter视频会议插件bspoke_jitsi_meet的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter视频会议插件bspoke_jitsi_meet的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
bspoke_jitsi_meet
是一个用于在 Flutter 应用中集成 Jitsi Meet 视频会议功能的插件。Jitsi Meet 是一个开源的视频会议解决方案,支持多人视频通话、屏幕共享、聊天等功能。bspoke_jitsi_meet
插件封装了 Jitsi Meet 的 API,使得在 Flutter 应用中集成视频会议功能变得更加容易。
以下是如何在 Flutter 项目中使用 bspoke_jitsi_meet
插件的步骤:
1. 添加依赖
首先,在 pubspec.yaml
文件中添加 bspoke_jitsi_meet
插件的依赖:
dependencies:
flutter:
sdk: flutter
bspoke_jitsi_meet: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get
来安装依赖。
2. 配置 Android 和 iOS 项目
Android 配置
在 android/app/build.gradle
文件中,确保 minSdkVersion
至少为 21:
android {
defaultConfig {
minSdkVersion 21
...
}
...
}
iOS 配置
在 ios/Podfile
文件中,确保 platform :ios
至少为 10.0:
platform :ios, '10.0'
然后运行 flutter pub get
和 pod install
来确保所有依赖项都已正确安装。
3. 使用 bspoke_jitsi_meet
插件
在 Flutter 代码中,导入 bspoke_jitsi_meet
插件并启动 Jitsi Meet 会议。
import 'package:flutter/material.dart';
import 'package:bspoke_jitsi_meet/bspoke_jitsi_meet.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
void startMeeting() async {
try {
var options = JitsiMeetingOptions(
roomName: "your_room_name", // 会议房间名称
serverURL: "https://meet.jit.si", // Jitsi Meet 服务器地址
subject: "Meeting Subject", // 会议主题
userDisplayName: "Your Name", // 用户显示名称
userEmail: "your_email@example.com", // 用户邮箱
audioMuted: false, // 是否静音
videoMuted: false, // 是否关闭视频
);
await BspokeJitsiMeet.joinMeeting(options);
} catch (error) {
print("Error: $error");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Jitsi Meet Example'),
),
body: Center(
child: ElevatedButton(
onPressed: startMeeting,
child: Text('Start Meeting'),
),
),
);
}
}
4. 处理会议事件
你可以通过监听 Jitsi Meet 的事件来处理会议中的各种状态变化,例如会议开始、结束、参与者加入等。
BspokeJitsiMeet.addListener(
JitsiMeetingListener(
onConferenceWillJoin: (url) {
print("Conference will join with url: $url");
},
onConferenceJoined: (url) {
print("Conference joined with url: $url");
},
onConferenceTerminated: (url) {
print("Conference terminated with url: $url");
},
onError: (error) {
print("Error: $error");
},
),
);
5. 清理资源
在会议结束后,记得移除监听器以释放资源。
@override
void dispose() {
BspokeJitsiMeet.removeAllListeners();
super.dispose();
}
6. 运行项目
现在你可以运行你的 Flutter 项目,点击按钮启动 Jitsi Meet 会议。
flutter run