Flutter视频会议插件custom_jitsi_meet_testtt的使用

介绍

Jitsi Meet 插件为 Flutter 提供了支持视频会议的功能,适用于 Android、iOS 和 Web 平台。
Jitsi Meet 是一个开源(Apache 许可)的基于 WebRTC 的 JavaScript 应用程序,通过 Jitsi Videobridge 提供高质量、安全且可扩展的视频会议服务。

目录

  1. 配置
  2. 加入会议
  3. 监听会议事件
  4. 程序化关闭会议
  5. 贡献

配置

IOS

注意: 示例代码可以在 XCode 12.2 和 Flutter 1.22.4 下编译。

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 文件中添加以下内容以获取相机和麦克风权限。

<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) 需要访问您的摄像头进行会议。</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) 需要访问您的麦克风进行会议。</string>

Android
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 <!-- 升级此版本 -->
AndroidManifest.xml

Jitsi Meet 的 AndroidManifest.xml 会与您的项目冲突,尤其是 application:label 字段。解决方法是在 android/app/src/main/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 // 必须为 Jitsi 提供支持
    targetSdkVersion 28
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}
Proguard

启用 ProGuard 后,如果没有 proguard-rules.pro 文件,发布 APK 将缺少 Flutter 包裹和 React Native 代码。在 android/app/build.gradle 中添加 ProGuard 支持。

buildTypes {
    release {
        signingConfig signingConfigs.debug
        
        // 添加以下三行以启用 ProGuard
        minifyEnabled true
        useProguard true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

然后在同一目录下创建 proguard-rules.pro 文件,并复制官方示例文件的内容。


Web

在 Web 端实现时,需要在 index.html 中包含 Jitsi JS 库。

<script src="https://meet.jit.si/external_api.js" type="application/javascript"></script>

示例代码:

<body>
  <!-- 此脚本安装 service_worker.js 以提供 PWA 功能 -->
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function () {
        navigator.serviceWorker.register('/flutter_service_worker.js');
      });
    }
  </script>
  <script src="https://meet.jit.si/external_api.js" type="application/javascript"></script>
  <script src="main.dart.js" type="application/javascript"></script>
</body>

加入会议

示例代码如下:

_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,格式为 <scheme>://<host>[/<path>]
userAvatarURL none 用户头像 URL。
token none JWT 令牌用于身份验证。
featureFlag 见下方 FeatureFlag 对象,用于启用/禁用功能并设置 Jitsi Meet SDK 的视频分辨率。

FeatureFlag 参数说明

标志 默认 (Android) 默认 (iOS) 描述
addPeopleEnabled true true 启用“添加人员”按钮。
calendarEnabled true auto 启用日历集成。
callIntegrationEnabled true true 启用呼叫集成(iOS 的 CallKit,Android 的 ConnectionService)。
closeCaptionsEnabled true true 启用字幕选项。
conferenceTimerEnabled true true 启用会议计时器。

监听会议事件

监听每场会议的事件:

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");
    },
    onError: (error) {
      debugPrint("_onError broadcasted: $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 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();

贡献

发送带有详细信息的拉取请求,清晰描述问题或功能。保持更改小且一次只针对一个问题。


示例代码

完整示例代码如下:

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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();
    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('Plugin example app'),
        ),
        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: [
                                    // 示例额外 JS 设置
                                    '<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: "Server URL",
                hintText: "提示:留空使用 meet.jitsi.si"),
          ),
          SizedBox(height: 14.0),
          TextField(
            controller: roomText,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "Room",
            ),
          ),
          SizedBox(height: 14.0),
          TextField(
            controller: subjectText,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "Subject",
            ),
          ),
          SizedBox(height: 14.0),
          TextField(
            controller: nameText,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "Display Name",
            ),
          ),
          SizedBox(height: 14.0),
          TextField(
            controller: emailText,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "Email",
            ),
          ),
          SizedBox(height: 14.0),
          TextField(
            controller: iosAppBarRGBAColor,
            decoration: InputDecoration(
                border: OutlineInputBorder(),
                labelText: "AppBar Color(IOS only)",
                hintText: "提示:必须是 HEX RGBA 格式"),
          ),
          SizedBox(height: 14.0),
          CheckboxListTile(
            title: Text("Audio Only"),
            value: isAudioOnly,
            onChanged: _onAudioOnlyChanged,
          ),
          SizedBox(height: 14.0),
          CheckboxListTile(
            title: Text("Audio Muted"),
            value: isAudioMuted,
            onChanged: _onAudioMutedChanged,
          ),
          SizedBox(height: 14.0),
          CheckboxListTile(
            title: Text("Video Muted"),
            value: isVideoMuted,
            onChanged: _onVideoMutedChanged,
          ),
          Divider(height: 48.0, thickness: 2.0),
          SizedBox(
            height: 64.0,
            width: double.maxFinite,
            child: ElevatedButton(
              onPressed: () {
                _joinMeeting();
              },
              child: Text(
                "Join Meeting",
                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,
    };
    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 = 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 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");
          },
          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");
  }
}
1 回复

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


custom_jitsi_meet_testtt 是一个用于在 Flutter 应用中集成视频会议功能的插件。它基于 Jitsi Meet,这是一个开源的视频会议解决方案。使用这个插件,你可以轻松地在你的 Flutter 应用中添加视频会议功能。

以下是如何在 Flutter 项目中使用 custom_jitsi_meet_testtt 插件的步骤:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 custom_jitsi_meet_testtt 插件的依赖。

dependencies:
  flutter:
    sdk: flutter
  custom_jitsi_meet_testtt: ^1.0.0  # 请使用最新版本

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

2. 配置 Android 和 iOS 项目

Android

android/app/src/main/AndroidManifest.xml 文件中添加以下权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

iOS

ios/Runner/Info.plist 文件中添加以下权限:

<key>NSCameraUsageDescription</key>
<string>We need access to your camera for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for audio calls.</string>

3. 使用插件

在你的 Dart 代码中,你可以使用 custom_jitsi_meet_testtt 插件来启动视频会议。

import 'package:flutter/material.dart';
import 'package:custom_jitsi_meet_testtt/custom_jitsi_meet_testtt.dart';

class VideoCallScreen extends StatefulWidget {
  @override
  _VideoCallScreenState createState() => _VideoCallScreenState();
}

class _VideoCallScreenState extends State<VideoCallScreen> {
  late JitsiMeet _jitsiMeet;

  @override
  void initState() {
    super.initState();
    _jitsiMeet = JitsiMeet();
  }

  void _startMeeting() async {
    var options = JitsiMeetingOptions(
      roomName: "your_room_name", // 会议室名称
      isAudioMuted: false,
      isVideoMuted: false,
    );

    await _jitsiMeet.joinMeeting(options);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Video Call'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: _startMeeting,
          child: Text('Start Meeting'),
        ),
      ),
    );
  }
}

4. 运行项目

确保你已经连接了 Android 或 iOS 设备,然后运行 flutter run 来启动你的应用。

5. 处理会议事件

你可以监听会议事件,例如会议结束、用户加入或离开等。

_jitsiMeet.onConferenceWillJoin.listen((event) {
  print("Conference will join: $event");
});

_jitsiMeet.onConferenceJoined.listen((event) {
  print("Conference joined: $event");
});

_jitsiMeet.onConferenceTerminated.listen((event) {
  print("Conference terminated: $event");
});

6. 自定义 Jitsi Meet 界面

你可以通过 JitsiMeetingOptions 来定制 Jitsi Meet 的界面,例如隐藏工具栏、设置背景颜色等。

var options = JitsiMeetingOptions(
  roomName: "your_room_name",
  isAudioMuted: false,
  isVideoMuted: false,
  featureFlags: {
    "toolbox.enabled": false,
    "filmstrip.enabled": false,
  },
);

7. 结束会议

你可以通过调用 _jitsiMeet.closeMeeting() 来结束会议。

void _endMeeting() async {
  await _jitsiMeet.closeMeeting();
}

8. 处理错误

确保你处理了可能的错误,例如网络问题或权限问题。

try {
  await _jitsiMeet.joinMeeting(options);
} catch (e) {
  print("Error joining meeting: $e");
}
回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!