Do you wonder what video calling apps actually mean? If not then no worries, you will get an idea soon while reading this blog. We all have come across virtual words more often nowadays. We are connecting with our employees, colleague, and others with the help of online platforms to share content and knowledge and report to one another. Video SDK came up with the idea of making an app that helps people with connecting remotely. During the meeting one can present their content to others, can raise their query by dropping a text, one can ask questions by turning on the mic and many more features are there you will get acquainted with at the end of this blog.

⚠️ Warning this blog are old version 0.0.14
If you are using old version here are Migration Notes
🆕 Here are new version 1.0.0 Quick Start Guide

4 Steps to Build a Video Calling App in Flutter

  • Develop and launch in both Android and iOS at the same time.

Prerequisite

Project Structure

Create a new Flutter App using the below command.

$ flutter create videosdk_flutter_quickstart

Your project structure's  lib directory should be as same as mentioned below

root
    ├── android
    ├── ios
    ├── lib
         ├── api.dart
         ├── join_screen.dart
         ├── main.dart
         ├── meeting_controls.dart
         ├── meeting_screen.dart
         ├── participant_tile.dart

Step 1:Video Calling Flutter SDK Integration For Android

1: import videosdk plugin from the video SDK

$ flutter pub add videosdk

//run this command to add http library to perform network call to generate meetingId

$ flutter pub add http

2: Update the AndroidManifest.xml  for the permissions we will be using to implement the audio and video features.

  • You can find the AndroidManifest.xml file at <project root>/android/app/src/main/AndroidManifest.xml
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET"/>

3: Also you will need to set your build settings to Java 8 because the official WebRTC jar now uses static methods in EglBase interface. Just add this to your app-level build.gradle

android {
    //...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
  • If necessary, in the same build.gradle you will need to increase minSdkVersion of defaultConfig up to 23 (currently default Flutter generator set it to 16).
  • If necessary, in the same build.gradle you will need to increase compileSdkVersion and targetSdkVersion up to 31 (currently default Flutter generator set it to 30).

Step 2:Video Calling Flutter SDK Integration For IoS

Add the following entries which allow your app to access the camera and microphone to your Info.plist file, located in <project root>/ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>

Step 3:Start Writing Your Code

1: Let's first set up api.dart file

Before jumping to anything else, you will write a function to generate a unique meetingId. You will require auth token, you can generate it using either by using videosdk-rtc-api-server-examples or generate it from the Video SDK Dashboard for development.

import 'dart:convert';
import 'package:http/http.dart' as http;

String token = "<Generated-from-dashboard>";

Future<String> createMeeting() async {
  final http.Response httpResponse = await http.post(
    Uri.parse("https://api.videosdk.live/v1/meetings"),
    headers: {'Authorization': token},
  );

  return json.decode(httpResponse.body)['meetingId'];
}

2: Now you will set up join-screen.dart file . The Joining screen will consist of

  • Create Button - This button will create a new meeting for you.
  • TextField for Meeting ID - This text field will contain the meeting ID you want to join.
  • Join Button - This button will join the meeting which you provided.

JoinScreen will accept 3 functions in constructor.

  • onCreateMeetingButtonPressed - invoked when Create Meeting button pressed
  • onJoinMeetingButtonPressed - invoked when Join button pressed
  • onMeetingIdChanged - invoked when MeetingId TextField value changed

Replace content of join_screen.dart file with code mentioned in code block

import 'package:flutter/material.dart';

class JoinScreen extends StatelessWidget {
  final void Function() onCreateMeetingButtonPressed;
  final void Function() onJoinMeetingButtonPressed;
  final void Function(String) onMeetingIdChanged;

  const JoinScreen({
    Key? key,
    required this.onCreateMeetingButtonPressed,
    required this.onJoinMeetingButtonPressed,
    required this.onMeetingIdChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
            child: const Text("Create Meeting"),
            onPressed: onCreateMeetingButtonPressed),
        const SizedBox(height: 16),
        TextField(
            decoration: const InputDecoration(
              hintText: "Meeting ID",
              border: OutlineInputBorder(),
            ),
            onChanged: onMeetingIdChanged),
        const SizedBox(height: 8),
        ElevatedButton(
          child: const Text("Join"),
          onPressed: onJoinMeetingButtonPressed,
        )
      ],
    );
  }
}

3 : We are done with the creation of join screen , now let's create a meeting controls of video calling app.

Create a new meeting_controls.dart file with stateless widget named MeetingControls.

The MeetingControls will consist of:

  • Leave Button - This button will leave the meeting.
  • Toggle Mic Button - This button will enable or disable the mic.
  • Toggle Webcam Button - This button will enable or disable Webcam.

MeetingControls will accept 3 functions in the constructor

  • onLeaveButtonPressed - invoked when the Leave button pressed
  • onToggleMicButtonPressed - invoked when Toggle Mic button pressed
  • onToggleWebcamButtonPressed - invoked when Toggle Webcam button pressed
import 'package:flutter/material.dart';

class MeetingControls extends StatelessWidget {
  final void Function() onToggleMicButtonPressed;
  final void Function() onToggleWebcamButtonPressed;
  final void Function() onLeaveButtonPressed;

  const MeetingControls({
    Key? key,
    required this.onToggleMicButtonPressed,
    required this.onToggleWebcamButtonPressed,
    required this.onLeaveButtonPressed,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        ElevatedButton(
          child: const Text("Leave"),
          onPressed: onLeaveButtonPressed,
        ),
        ElevatedButton(
          child: const Text("Toggle Mic"),
          onPressed: onToggleMicButtonPressed,
        ),
        ElevatedButton(
          child: const Text("Toggle WebCam"),
          onPressed: onToggleWebcamButtonPressed,
        )
      ],
    );
  }
}

4: Now we will create a participantTile for each participant who joins the meeting

For that create a participant_tile.dart file and create ParticipantTile StateLess Widget.

The ParticipantTile will consist of:

  • RTCVideoView - This will show remote participant video stream.

ParticipantTile will accept Stream in constructor

  • stream - remote participant video stream
import 'package:flutter/material.dart';
import 'package:videosdk/rtc.dart';

class ParticipantTile extends StatelessWidget {
  final Stream stream;
  const ParticipantTile({
    Key? key, required this.stream,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: SizedBox(
        height: 200,
        width: 200,
        child: RTCVideoView(
          stream.renderer!,
          objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
        ),
      ),
    );
  }
}

5:  In this step, we are going to create meetingScreen

For that, create meeting_screen.dart file and create MeetingScreen as a StateFul Widget.

The MeetingScreen will consist of:

  • MeetingBuilder - This will build Meeting based on the given configuration.

MeetingScreen will accept meetingId and token in the constructor

  • meetingId - meetingId, you want to join
  • token - VideoSdk Auth token
import 'package:flutter/material.dart';
import 'package:videosdk/rtc.dart';
import 'meeting_controls.dart';
import 'participant_tile.dart';

class MeetingScreen extends StatefulWidget {
  final String meetingId;
  final String token;
  final void Function() leaveMeeting;

  const MeetingScreen(
      {Key? key,
      required this.meetingId,
      required this.token,
      required this.leaveMeeting})
      : super(key: key);

  @override
  State<MeetingScreen> createState() => _MeetingScreenState();
}

class _MeetingScreenState extends State<MeetingScreen> {
  Map<String, Stream?> participantVideoStreams = {};

  bool micEnabled = true;
  bool webcamEnabled = true;

  void setParticipantStreamEvents(Participant participant) {
    participant.on(Events.streamEnabled, (Stream stream) {
      if (stream.kind == 'video') {
        setState(() => participantVideoStreams[participant.id] = stream);
      }
    });

    participant.on(Events.streamDisabled, (Stream stream) {
      if (stream.kind == 'video') {
        setState(() => participantVideoStreams.remove(participant.id));
      }
    });
  }

  void setMeetingEventListener(Meeting _meeting) {
    setParticipantStreamEvents(_meeting.localParticipant);
    _meeting.on(
      Events.participantJoined,
      (Participant participant) => setParticipantStreamEvents(participant),
    );
    _meeting.on(Events.participantLeft, (String participantId) {
      if (participantVideoStreams.containsKey(participantId)) {
        setState(() => participantVideoStreams.remove(participantId));
      }
    });
    _meeting.on(Events.meetingLeft, () {
      participantVideoStreams.clear();
      widget.leaveMeeting();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MeetingBuilder(
      meetingId: widget.meetingId,
      token: widget.token,
      micEnabled: micEnabled,
      webcamEnabled: webcamEnabled,
      displayName: "Yash Chudasama",
      notification: const NotificationInfo(
          title: "Video SDK",
          message: "Video SDK is sharing screen in the meeting",
          icon: "notification_share"),
      builder: (_meeting) {
        setMeetingEventListener(_meeting);
        return SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text("Meeting ID: ${widget.meetingId}"),
              MeetingControls(
                onToggleMicButtonPressed: () {
                  micEnabled ? _meeting.muteMic() : _meeting.unmuteMic();
                  micEnabled = !micEnabled;
                },
                onToggleWebcamButtonPressed: () {
                  webcamEnabled
                      ? _meeting.disableWebcam()
                      : _meeting.enableWebcam();
                  webcamEnabled = !webcamEnabled;
                },
                onLeaveButtonPressed: () => _meeting.leave(),
              ),
              ...participantVideoStreams.values
                  .map(
                    (e) => ParticipantTile(
                      stream: e!,
                    ),
                  )
                  .toList(),
            ],
          ),
        );
      },
    );
  }
}

6: What is the purpose of doing all the above steps without changing our main.dart file.

So in this step we will see about changing main.dart file where we will provide a condition and based on that our joinScreen or meetingScreen will get appeared.

Remove boilerplate code from the main.dart. Create VideoSDKQuickStart StatefulWidget and pass it to MaterialApp.

VideoSDKQuickStart widget will return MeetingScreen if the meeting is active, otherwise, return JoinScreen.

import 'package:flutter/material.dart';
import 'api.dart';
import 'join_screen.dart';
import 'meeting_screen.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'VideoSDK QuickStart',
      home: VideoSDKQuickStart(),
    ),
  );
}

class VideoSDKQuickStart extends StatefulWidget {
  const VideoSDKQuickStart({Key? key}) : super(key: key);

  @override
  State<VideoSDKQuickStart> createState() => _VideoSDKQuickStartState();
}

class _VideoSDKQuickStartState extends State<VideoSDKQuickStart> {
  String meetingId = "";
  bool isMeetingActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("VideoSDK QuickStart"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: isMeetingActive
            ? MeetingScreen(
                meetingId: meetingId,
                token: token,
                leaveMeeting: () {
                  setState(() => isMeetingActive = false);
                },
              )
            : JoinScreen(
                onMeetingIdChanged: (value) => meetingId = value,
                onCreateMeetingButtonPressed: () async {
                  meetingId = await createMeeting();
                  setState(() => isMeetingActive = true);
                },
                onJoinMeetingButtonPressed: () {
                  setState(() => isMeetingActive = true);
                },
              ),
      ),
    );
  }
}

Step 4:Run Your Code Now

$ flutter run

Conclusion

With this, we successfully built the video chat app using the video SDK in Flutter. If you wish to add functionalities like chat messaging, and screen sharing, you can always check out our documentation. If you face any difficulty with the implementation you can connect with us on our discord community.