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文件中添加NSCameraUsageDescriptionNSMicrophoneUsageDescription字段。

<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 getpod 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
回到顶部