How to make a video calling app using Jitsi in Flutter

In this tutorial I show you how to use Jitsi for video calling app in flutter.
Jitsi is a free, open-source, multiplatform voice and video chat application. You can connect Jitsi with flutter app to make video call, audio call in your app. This blog post is going to teach you how to do that!
To connect Jitsi with your Flutter app, all you need is the jitsi_meet
flutter package in your pubspec.yaml
file. You can find jitsi_meet
latest version from here https://pub.dev/packages/jitsi_meet
Steps and functionalities:
- Meeting code: This is a unique string which identify a meeting
- User can create a meeting code and share with others
- To join a meeting user will require to enter the meeting code for that specific meeting
The end result would look like this =>
Setting up the environment is a bit complex, but if you follow this correctly it will work like a charm.
Requirments and Configuration:
IOS
Podfile: you need to disable BITCODE and make sure that the platform is of 11 or above.
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
Note: If you don’t find the podfile then you need to build the app for ios using
then
flutter build ios
Podfile
andPodfile.lock
will be created for you in theios
directory.
Info.plist: You need to give permission for Camera and microfone usages. Add the below code in info.plist file located in ios/Runner
folder
<key>NSCameraUsageDescription</key> <string>$(PRODUCT_NAME) MyApp needs access to your camera for meetings.</string> <key>NSMicrophoneUsageDescription</key> <string>$(PRODUCT_NAME) MyApp needs access to your microphone for meetings.</string>
ANDROID:
Gradle: dependencies of build tools gradle located in the directory android/build.gradle
need to be minimum 3.6.3
dependencies { classpath 'com.android.tools.build:gradle:3.6.3' <!-- Upgrade this --> classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }
Distribution gradle wrapper need to be minimum 5.6.4 located in android/gradle/wrapper/gradle-wrapper.properties
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 <!-- Upgrade this -->
AndroidManifest.xml:
Jitsi Meet’s SDK AndroidManifest.xml will conflict the application:label
field with your app. So to remove that, go into android/app/src/main/AndroidManifest.xml
and add the tools library in manifest tag and tools:replace="android:label"
to the application tag as shown below.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="yourpackage.com" xmlns:tools="http://schemas.android.com/tools"> <!-- Add this and make sure its inside the manifest tag--> <application tools:replace="android:label" android:label="My Application" android:icon="@mipmap/ic_launcher"> ... </application> ... </manifest>
Minimum SDK Version 23: Update your minimum sdk version to 23 in android/app/build.gradle
defaultConfig { applicationId "com.gunschu.jitsi_meet_example" minSdkVersion 23 //Required for Jitsi targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName }
Proguard: Jitsi’s SDK enables proguard, so you need to create a
file otherwise, your release apk build will be missing the Flutter Wrapper as well as react-native code. In your Flutter project’sproguard-rules.pro
android/app/build.gradle
file, add proguard support as shown below-
buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug // Add below 3 lines for proguard minifyEnabled true useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
Now you need to create a
file in the same directory i.e inside proguard-rules.pro
android/app/
folder and paste the content form https://github.com/jitsi/jitsi-meet/blob/master/android/app/proguard-rules.pro
Note: If you do not create the
proguard-rules.pro
file, then your app will crash when you try to join a meeting or the meeting screen tries to open but closes immediately.
Congratulations! Now you are ready with the configuration for using jitsi in your app.
Now let’s start coding…
Jitsi provide the features to setup your own server for video calling. Otherwise you can use the default server provided by jitsi. I will use jitsi’s default server https://meet.jit.si/
App Implementation
In the main.dart
file, I added a
which opens a bottom sheet which uses a floatingActionButton
widget from a different file called statefull
video_call.dart
under lib folder. All the functionality for video calling is implemented in video_call.dart
file. Code for main.dart
file is given below-
import 'package:flutter/material.dart'; import 'package:video_call_jitsi/video_call.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Video Call Using Jitsi"), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) { return FractionallySizedBox( heightFactor: 0.8, child: VideoCall(), ); }, ); }, tooltip: 'Video Call', child: Icon(Icons.video_call), ), ); } }

FloatingActionButton
video_call.dart: For joining in the video call, we need to create a meeting code and then using the same meeting code one can join in the meeting.
Create meeting code: I defined a function called
for randomly generating 8 digit meeting code.createCode
void createCode() { var r = Random(); const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; var code = List.generate(8, (index) => _chars[r.nextInt(_chars.length)]).join(); setState(() { meetingCode = code; }); }
Copy Meeting Code: For Copying generated code I used flutter default clipboard and then a snackbar is being displyed.
copyMeetingCode(BuildContext context) { Clipboard.setData(ClipboardData(text: meetingCode)).then((value) { final snackBar = SnackBar(content: Text('Meeting Code Copied!')); ScaffoldMessenger.of(context).showSnackBar(snackBar); }); }
Joining Meeting
For joining a meeting, we need to call
method which takes some options including predefined flags, meeting code , subject, server url (you can pass null for using jitsi’s default server), username, user email etc. You can find all the flags and options details from https://pub.dev/packages/jitsi_meetJitsiMeet joinMeeting
You can also fetch user details such as name, email etc from database dynamically and use them as options during joing the meeting. The code for joining meeting is given below and you can read comments for better understanding. You can enable or disable disable different option like
etc by setting them as audioOnly, audioMuted, videoMuted
true
or false
.
joinMeeting() async { if (meetingCodeController.text.isNotEmpty) { try { // Enable or disable any feature flag here // If feature flag are not provided, default values will be used // Full list of feature flags (and defaults) available in the README Map<FeatureFlagEnum, bool> featureFlags = { FeatureFlagEnum.WELCOME_PAGE_ENABLED: false, }; if (!kIsWeb) { // Here is an example, disabling features for each platform if (Platform.isAndroid) { // Disable ConnectionService usage on Android to avoid issues (see README) featureFlags[FeatureFlagEnum.CALL_INTEGRATION_ENABLED] = false; } else if (Platform.isIOS) { // Disable PIP on iOS as it looks weird featureFlags[FeatureFlagEnum.PIP_ENABLED] = false; } } // Define meetings options here var options = JitsiMeetingOptions(room: meetingCode) ..serverURL = null ..subject = "Subject Here" ..userDisplayName = "Username" ..userEmail = "user email" ..iosAppBarRGBAColor = null ..audioOnly = false ..audioMuted = true ..videoMuted = false ..featureFlags.addAll(featureFlags) ..webOptions = { "roomName": meetingCode, "width": "100%", "height": "100%", "enableWelcomePage": false, "chromeExtensionBanner": null, "userInfo": {"displayName": "user name"} }; 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"); }, ), ], ), ); } catch (e) { print(e.toString()); } } }
I also
listeners on
initialised
for different activities during video call like when a user join the meeting, or when a conference is terminated, or if an error occur. I just used these listener to print the message, you can do anything you want accordingly.
initState
@override void initState() { JitsiMeet.addListener(JitsiMeetingListener( onConferenceWillJoin: _onConferenceWillJoin, onConferenceJoined: _onConferenceJoined, onConferenceTerminated: _onConferenceTerminated, onError: _onError, )); super.initState(); }
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"); }
The complete code for
is given below-video_call.dart
import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jitsi_meet/jitsi_meet.dart'; class VideoCall extends StatefulWidget { VideoCall({Key? key}) : super(key: key); @override _VideoCallState createState() => _VideoCallState(); } class _VideoCallState extends State<VideoCall> { var meetingCodeController = TextEditingController(); var meetingCode = ""; @override void initState() { JitsiMeet.addListener(JitsiMeetingListener( onConferenceWillJoin: _onConferenceWillJoin, onConferenceJoined: _onConferenceJoined, onConferenceTerminated: _onConferenceTerminated, onError: _onError, )); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( child: Form( child: Column( mainAxisSize: MainAxisSize.max, children: <Widget>[ Column( children: [ Card( child: Column( children: [ TextFormField( controller: meetingCodeController, keyboardType: TextInputType.text, decoration: InputDecoration( border: OutlineInputBorder(), ), validator: (value) { return value!.isEmpty ? "Code Required" : null; }, ), Padding( padding: const EdgeInsets.all(15.0), child: SizedBox( child: MaterialButton( textColor: Colors.red, splashColor: Colors.grey.withOpacity(0.2), padding: EdgeInsets.only( top: 15, bottom: 15, left: 20, right: 20, ), child: Text("Join Meeting"), shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(10), side: BorderSide( color: Theme.of(context).primaryColor), ), onPressed: () { print("join meeting pressed"); print(meetingCodeController.text); joinMeeting(); }, ), ), ), ], ), ), Divider(), SizedBox( height: 70, ), Card( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( meetingCode, style: TextStyle( color: Colors.orange, fontSize: 24, ), ), Visibility( visible: meetingCode.isNotEmpty, child: TextButton.icon( icon: Icon( Icons.copy, color: Colors.white, ), label: Text( "Copy Code", style: TextStyle(color: Colors.white), ), onPressed: () { copyMeetingCode(context); }, style: ButtonStyle( backgroundColor: MaterialStateProperty.all( Colors.blueGrey), elevation: MaterialStateProperty.all(5), padding: MaterialStateProperty.all(EdgeInsets.only( left: 20, right: 20, top: 15, bottom: 15, )), foregroundColor: MaterialStateProperty.all(Colors.white24), ), ), ), ], ), Padding( padding: const EdgeInsets.all(20), child: SizedBox( child: MaterialButton( textColor: Colors.red, splashColor: Colors.grey.withOpacity(0.2), padding: EdgeInsets.only( top: 15, bottom: 15, left: 20, right: 20, ), child: Text("Create Meeting Code"), shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(10), side: BorderSide( color: Theme.of(context).primaryColor, ), ), onPressed: () { print("create meeting code clicked"); createCode(); }, ), ), ), ], ), ), ], ), ], ), ), ), ); } void createCode() { var r = Random(); const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; var code = List.generate(8, (index) => _chars[r.nextInt(_chars.length)]).join(); setState(() { meetingCode = code; }); } copyMeetingCode(BuildContext context) { Clipboard.setData(ClipboardData(text: meetingCode)).then((value) { final snackBar = SnackBar(content: Text('Meeting Code Copied!')); ScaffoldMessenger.of(context).showSnackBar(snackBar); }); } joinMeeting() async { if (meetingCodeController.text.isNotEmpty) { try { // Enable or disable any feature flag here // If feature flag are not provided, default values will be used // Full list of feature flags (and defaults) available in the README Map<FeatureFlagEnum, bool> featureFlags = { FeatureFlagEnum.WELCOME_PAGE_ENABLED: false, }; if (!kIsWeb) { // Here is an example, disabling features for each platform if (Platform.isAndroid) { // Disable ConnectionService usage on Android to avoid issues (see README) featureFlags[FeatureFlagEnum.CALL_INTEGRATION_ENABLED] = false; } else if (Platform.isIOS) { // Disable PIP on iOS as it looks weird featureFlags[FeatureFlagEnum.PIP_ENABLED] = false; } } // Define meetings options here var options = JitsiMeetingOptions(room: meetingCode) ..serverURL = null ..subject = "Subject Here" ..userDisplayName = "Username" ..userEmail = "user email" ..iosAppBarRGBAColor = null ..audioOnly = false ..audioMuted = true ..videoMuted = false ..featureFlags.addAll(featureFlags) ..webOptions = { "roomName": meetingCode, "width": "100%", "height": "100%", "enableWelcomePage": false, "chromeExtensionBanner": null, "userInfo": {"displayName": "user name"} }; 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"); }, ), ], ), ); } catch (e) { print(e.toString()); } } } 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"); } }
Now you have successfully built a free video calling app in flutter
For the full source code, you can visit https://github.com/krexal/flutter-video-call-jitsi
Thanks for reading.
Nice and helpful one.
Thank you so much for your comment. Happy to know that you found it useful.