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 flutter build ios  then Podfile and Podfile.lock will be created for you in the ios 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 proguard-rules.pro file otherwise, your release apk build will be missing the Flutter Wrapper as well as react-native code. In your Flutter project’s 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 proguard-rules.pro file in the same directory i.e inside 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 floatingActionButton which opens a bottom sheet which uses a statefull widget from a different file called 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),
      ),
    );
  }
}
video calling app in flutter
Home Page With 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 createCode for randomly generating 8 digit meeting code.

 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 JitsiMeet joinMeeting 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_meet

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 audioOnly, audioMuted, videoMuted etc by setting them as 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 initialised listeners on initState 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.

@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 video_call.dart is given below-

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.

You may also like...

2 Responses

  1. Krishna singhal says:

    Nice and helpful one.

Leave a Reply

Your email address will not be published.