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

1 回复

更多关于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
回到顶部