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

1 回复

更多关于Flutter视频会议插件eko_jitsi的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用eko_jitsi插件来集成Jitsi视频会议功能的代码示例。eko_jitsi是一个Flutter插件,它封装了Jitsi Meet的功能,使得在Flutter应用中集成视频会议变得简单。

首先,确保你已经在pubspec.yaml文件中添加了eko_jitsi依赖:

dependencies:
  flutter:
    sdk: flutter
  eko_jitsi: ^latest_version # 替换为最新版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤使用eko_jitsi插件:

  1. 导入必要的包
import 'package:flutter/material.dart';
import 'package:eko_jitsi/eko_jitsi.dart';
  1. 创建一个函数来启动Jitsi会议
Future<void> startJitsiMeeting(BuildContext context) async {
  final options = JitsiMeetingOptions(
    room: 'YourJitsiRoomName', // 替换为你的Jitsi房间名
    serverURL: 'https://meet.jit.si', // Jitsi服务器的URL,默认为meet.jit.si
    user: JitsiUserOptions(displayName: 'YourDisplayName'), // 用户的显示名称
    audioOnly: false, // 是否仅音频
    videoMuted: false, // 视频是否静音
    audioMuted: false, // 音频是否静音
    subject: 'Meeting Subject', // 会议主题
    password: 'YourMeetingPassword', // 如果会议设置了密码
  );

  try {
    await JitsiMeet.joinMeeting(options);
  } catch (e) {
    // 处理异常,例如用户取消加入会议
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Failed to join meeting: $e')),
    );
  }
}
  1. 在UI中添加一个按钮来启动会议
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Jitsi Meet Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Jitsi Meet Example'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => startJitsiMeeting(context),
          child: Text('Join Meeting'),
        ),
      ),
    );
  }
}

在这个示例中,我们创建了一个简单的Flutter应用,其中包含一个按钮。当用户点击该按钮时,会调用startJitsiMeeting函数,该函数会启动Jitsi视频会议。

请确保替换YourJitsiRoomNameYourDisplayNameYourMeetingPassword(如果适用)为你的实际值。

此外,如果你希望自定义更多选项,可以查阅eko_jitsi插件的官方文档来了解所有可用的配置选项。

希望这个示例能帮助你在Flutter项目中成功集成Jitsi视频会议功能!

回到顶部