DEV Community

Cover image for Build a Flutter Video Calling App in 4 Steps with Video SDK
Bhavi Gotawala for Video SDK

Posted on • Originally published at videosdk.live

Build a Flutter Video Calling App in 4 Steps with Video SDK

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, knowledge and to 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 acquaintance of at the end of this blog.

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 one flutter project first by writing the following command

$ flutter create videosdk_flutter_quickstart

Enter fullscreen mode Exit fullscreen mode

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

root-Folder Name
   ├── ...
   ├── lib
    ├── join_screen.dart
        ├── main.dart
        ├── meeting_screen.dart
        ├── participant_grid_view.dart
        ├── participant_tile.dart

Enter fullscreen mode Exit fullscreen mode

Step 1:Flutter SDK Integration For Android

1 : import flutter sdk from video SDK

$ flutter pub add videosdk

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

Enter fullscreen mode Exit fullscreen mode

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 /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" />

Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode
  • If necessary, in the same build.gradle you will need to increase minSdkVersion of defaultConfig up to 21 (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: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 /ios/Runner/Info.plist :

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

Enter fullscreen mode Exit fullscreen mode

Step 3:Start Writing Your Code

1 : Let's create a join-screen now.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.

Create a new dart file join_screen.dart which will contain our Stateful Widget named JoinScreen.

Replace the _token with the sample token you generated from the VideoSDK Dashboard.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:videosdk_flutter_quickstart/meeting_screen.dart';

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

  @override
  _JoinScreenState createState() => _JoinScreenState();
}

class _JoinScreenState extends State<JoinScreen> {
  //Repalce the token with the sample token you generated from the VideoSDK Dashboard
  String _token ="";

  String _meetingID = "";

  @override
  Widget build(BuildContext context) {
    final ButtonStyle _buttonStyle = TextButton.styleFrom(
      primary: Colors.white,
      backgroundColor: Theme.of(context).primaryColor,
      textStyle: const TextStyle(
        fontWeight: FontWeight.bold,
      ),
    );
    return Scaffold(
      appBar: AppBar(
        title: const Text("VideoSDK RTC"),
      ),
      backgroundColor: Theme.of(context).backgroundColor,
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [

          ],
        ),
      ),
    );
  }

}

Enter fullscreen mode Exit fullscreen mode

Update the JoinScreen with two buttons and a text field in the children property of column widget.

class _JoinScreenState extends State<JoinScreen> {
  ...
  @override
  Widget build(BuildContext context) {
    ...
    return Scaffold(
      ...
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            //Button to Create Meeting
            TextButton(
              style: _buttonStyle,
              onPressed: () async {
                //When clicked
                //Generate a meetingId
                _meetingID = await createMeeting();

                //Navigatet to MeetingScreen
                navigateToMeetingScreen();
              },
              child: const Text("CREATE MEETING"),
            ),
            SizedBox(height: 20),
            const Text(
              "OR",
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: Colors.white,
                fontSize: 24,
              ),
            ),
            SizedBox(height: 20),
            //Textfield for entering meetingId
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 32.0),
              child: TextField(
                onChanged: (meetingID) => _meetingID = meetingID,
                decoration: InputDecoration(
                  border: const OutlineInputBorder(),
                  fillColor: Theme.of(context).primaryColor,
                  labelText: "Enter Meeting ID",
                  hintText: "Meeting ID",
                  prefixIcon: const Icon(
                    Icons.keyboard,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
            SizedBox(height: 20),
            //Button to join the meeting
            TextButton(
              onPressed: () async {
                //Navigate to MeetingScreen
                navigateToMeetingScreen();
              },
              style: _buttonStyle,
              child: const Text("JOIN MEETING"),
            )
          ],
        ),
      ),
    );
  }

  //This is method is called to navigate to MeetingScreen.
  //It passes the token and meetingId to MeetingScreen as parameters
  void navigateToMeetingScreen(){
    Navigator.push(
      context,
      MaterialPageRoute(
        //MeetingScreen is created in upcomming steps
        builder: (context) => MeetingScreen(
          token: _token,
          meetingId: _meetingID,
          displayName: "John Doe",
        ),
      ),
    );
  }

}

Enter fullscreen mode Exit fullscreen mode

Add the createMeeting() in the JoinScreen which will generate a new meeting id.

class _JoinScreenState extends State<JoinScreen> {

  //...other variables

  //...build method

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

    final meetingId = json.decode(meetingIdResponse.body)['meetingId'];
    return meetingId;
  }
}

Enter fullscreen mode Exit fullscreen mode

Make the JoinScreen as your home in the main.dart as shown below.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: JoinScreen(), //change to this
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Create a new meeting_screen.dart which will have a Stateful widget named MeetingScreen.

MeetingScreen accepts the following parameter:

  • meetingId : This will be the meeting Id we will joining
  • token : Auth Token to configure the Meeting
  • displayName : Name with which the participant will be joined
  • micEnabled : (Optional) If true, the mic will be on when you join the meeting else it will be off.
  • webcamEnabled : (Optional) If true, the webcam will be on when you join the meeting else it will be off.
import 'package:flutter/material.dart';
import 'package:videosdk/rtc.dart';
import 'package:videosdk_flutter_quickstart/join_screen.dart';
import 'package:videosdk_flutter_quickstart/participant_grid_view.dart';

class MeetingScreen extends StatefulWidget {

  //add the following parameters for your MeetingScreen
  final String meetingId, token, displayName;
  final bool micEnabled, webcamEnabled;
  const MeetingScreen({
    Key? key,
    required this.meetingId,
    required this.token,
    required this.displayName,
    this.micEnabled = true,
    this.webcamEnabled = true
  }) : super(key: key);

  @override
  _MeetingScreenState createState() => _MeetingScreenState();
}

class _MeetingScreenState extends State<MeetingScreen> {

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we will update _MeetingScreenState to use the MeetingBuilder to create our meeting.

We will pass the required parameters to the MeetingBuilder.

class _MeetingScreenState extends State<MeetingScreen> {

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPopScope,
      //MeetingBuilder is a class of @videosdk/rtc.dart
      child: MeetingBuilder(
        meetingId: widget.meetingId,
        displayName: widget.displayName,
        token: widget.token,
        micEnabled: widget.micEnabled,
        webcamEnabled: widget.webcamEnabled,
        notification: const NotificationInfo(
          title: "Video SDK",
          message: "Video SDK is sharing screen in the meeting",
          icon: "notification_share", // drawable icon name
        ),
        builder: (_meeting) {

        },
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we will add meeting , videoStream , audioStream which will store the meeting and the streams for local participant.

class _MeetingScreenState extends State<MeetingScreen> {

  Meeting? meeting;

  Stream? videoStream;
  Stream? audioStream;

  //...build

}

Enter fullscreen mode Exit fullscreen mode

Now we will update the builder to generate a meeting view.

  • Adding the Event listeners for the meeting and setting the state of our local meeting
class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPopScope,
      child: MeetingBuilder(
        //meetingConfig

        builder: (_meeting) {
          // Called when joined in meeting
          _meeting.on(
            Events.meetingJoined,
            () {
              setState(() {
                meeting = _meeting;
              });

              // Setting meeting event listeners
              setMeetingListeners(_meeting);
            },
          );
        }
      ),
    );
  }

  void setMeetingListeners(Meeting meeting) {
    // Called when meeting is ended
    meeting.on(Events.meetingLeft, () {
      Navigator.pushAndRemoveUntil(
          context,
          MaterialPageRoute(builder: (context) => const JoinScreen()),
          (route) => false);
    });

    // Called when stream is enabled
    meeting.localParticipant.on(Events.streamEnabled, (Stream _stream) {
      if (_stream.kind == 'video') {
        setState(() {
          videoStream = _stream;
        });
      } else if (_stream.kind == 'audio') {
        setState(() {
          audioStream = _stream;
        });
      }
    });

    // Called when stream is disabled
    meeting.localParticipant.on(Events.streamDisabled, (Stream _stream) {
      if (_stream.kind == 'video' && videoStream?.id == _stream.id) {
        setState(() {
          videoStream = null;
        });
      } else if (_stream.kind == 'audio' && audioStream?.id == _stream.id) {
        setState(() {
          audioStream = null;
        });
      }
    });
  }

}

Enter fullscreen mode Exit fullscreen mode
  • We will be showing a Waiting to join screen until the meeting is joined and then show the meeting view.

class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPopScope,
      child: MeetingBuilder(
        //meetingConfig

        builder: (_meeting) {
          //_meeting listener

          // Showing waiting screen
          if (meeting == null) {
            return Scaffold(
              body: Center(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const CircularProgressIndicator(),
                    SizedBox(height: 20),
                    const Text("waiting to join meeting"),
                  ],
                ),
              ),
            );
          }

          //Meeting View
          return Scaffold(
            backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
            appBar: AppBar(
              title: Text(widget.meetingId),
            ),
            body: Column(
              children: [

              ],
            ),
          );
        }
      ),
    );
  }

}

Enter fullscreen mode Exit fullscreen mode
  • Inside our meeting view we will add a ParticipantGrid and three action buttons.
class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPopScope,
      child: MeetingBuilder(
        //...meetingConfig

        builder: (_meeting) {
          //... _meeting listener

          //...Waiting Screen UI

          //Meeting View
          return Scaffold(
            backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
            appBar: AppBar(
              title: Text(widget.meetingId),
            ),
            body: Column(
              children: [
                Expanded(
                  //ParticipantGridView will be created in further steps!
                  child: ParticipantGridView(meeting: meeting!),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: () => {
                        if (audioStream != null)
                          {_meeting.muteMic()}
                        else
                          {_meeting.unmuteMic()}
                      },
                      child: Text("Mic"),
                    ),
                    ElevatedButton(
                      onPressed: () => {
                        if (videoStream != null)
                          {_meeting.disableWebcam()}
                        else
                          {_meeting.enableWebcam()}
                      },
                      child: Text("Webcam"),
                    ),
                    ElevatedButton(
                      onPressed: () => {_meeting.leave()},
                      child: Text("Leave"),
                    ),
                  ],
                )
              ],
            ),
          );
        }
      ),
    );
  }

}

Enter fullscreen mode Exit fullscreen mode
  • Add the _onWillPopScope() which will handle the meeting leave on back button click.
class _MeetingScreenState extends State<MeetingScreen> {

  //...other declarations

  //...build method

  //... setMeetingListeners

  Future<bool> _onWillPopScope() async {
    meeting?.leave();
    return true;
  }
}

Enter fullscreen mode Exit fullscreen mode

3 : Next we will be creating the ParticipantGridView which will be used to show the participant's view.

ParticipantGridView maps each participant with a ParticipantTile.

It updates the participant's list whenever someone leaves or joins the meeting using the participantJoined and participantLeft event listeners on the meeting.

create participant_grid_view.dart file which has a Stateful widget named as ParticipantGridView.

import 'package:flutter/material.dart';
import 'package:videosdk/rtc.dart';

import 'participant_tile.dart';

class ParticipantGridView extends StatefulWidget {
  final Meeting meeting;
  const ParticipantGridView({
    Key? key,
    required this.meeting,
  }) : super(key: key);

  @override
  State<ParticipantGridView> createState() => _ParticipantGridViewState();
}

class _ParticipantGridViewState extends State<ParticipantGridView> {
  Participant? localParticipant;
  Map<String, Participant> participants = {};

  @override
  void initState() {
    // Initialize participants
    localParticipant = widget.meeting.localParticipant;
    participants = widget.meeting.participants;

    // Setting meeting event listeners
    setMeetingListeners(widget.meeting);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 2,
      children: [
        //This Participant Tile will hold local participants view
        ParticipantTile(
          participant: localParticipant!,
          isLocalParticipant: true,
        ),
        //This will map all other participants
        ...participants.values
            .map((participant) => ParticipantTile(participant: participant))
            .toList()
      ],
    );
  }

  void setMeetingListeners(Meeting _meeting) {
    // Called when participant joined meeting
    _meeting.on(
      Events.participantJoined,
      (Participant participant) {
        final newParticipants = participants;
        newParticipants[participant.id] = participant;
        setState(() {
          participants = newParticipants;
        });
      },
    );

    // Called when participant left meeting
    _meeting.on(
      Events.participantLeft,
      (participantId) {
        final newParticipants = participants;

        newParticipants.remove(participantId);
        setState(() {
          participants = newParticipants;
        });
      },
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

4 : Creating the ParticipantTile which will be placed inside the GridView and for that create participant_tile.dart file

import 'package:flutter/material.dart';
import 'package:videosdk/rtc.dart';

class ParticipantTile extends StatefulWidget {
  final Participant participant;
  final bool isLocalParticipant;
  const ParticipantTile(
      {Key? key, required this.participant, this.isLocalParticipant = false})
      : super(key: key);

  @override
  State<ParticipantTile> createState() => _ParticipantTileState();
}

class _ParticipantTileState extends State<ParticipantTile> {
  Stream? videoStream;
  Stream? audioStream;

  @override
  Widget build(BuildContext context) {
    return Container();
  }

}

Enter fullscreen mode Exit fullscreen mode
  • Now we will initialise the state with the current streams of the Participant and add listeners for stream change

class _ParticipantTileState extends State<ParticipantTile> {
  Stream? videoStream;
  Stream? audioStream;

  @override
  void initState() {
    _initStreamListeners();
    super.initState();

    widget.participant.streams.forEach((key, Stream stream) {
      setState(() {
        if (stream.kind == 'video') {
          videoStream = stream;
        } else if (stream.kind == 'audio') {
          audioStream = stream;
        }
      });
    });
  }

  //... build

  _initStreamListeners() {
    widget.participant.on(Events.streamEnabled, (Stream _stream) {
      setState(() {
        if (_stream.kind == 'video') {
          videoStream = _stream;
        } else if (_stream.kind == 'audio') {
          audioStream = _stream;
        }
      });
    });

    widget.participant.on(Events.streamDisabled, (Stream _stream) {
      setState(() {
        if (_stream.kind == 'video' && videoStream?.id == _stream.id) {
          videoStream = null;
        } else if (_stream.kind == 'audio' && audioStream?.id == _stream.id) {
          audioStream = null;
        }
      });
    });

    widget.participant.on(Events.streamPaused, (Stream _stream) {
      setState(() {
        if (_stream.kind == 'video' && videoStream?.id == _stream.id) {
          videoStream = _stream;
        } else if (_stream.kind == 'audio' && audioStream?.id == _stream.id) {
          audioStream = _stream;
        }
      });
    });

    widget.participant.on(Events.streamResumed, (Stream _stream) {
      setState(() {
        if (_stream.kind == 'video' && videoStream?.id == _stream.id) {
          videoStream = _stream;
        } else if (_stream.kind == 'audio' && audioStream?.id == _stream.id) {
          audioStream = _stream;
        }
      });
    });
  }

}

Enter fullscreen mode Exit fullscreen mode
  • Now we will create RTCVideoView to show the participant stream and also add other components like the name and mic status indicator of the participant.
class _ParticipantTileState extends State<ParticipantTile> {
  Stream? videoStream;
  Stream? audioStream;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(4.0),
      decoration: BoxDecoration(
        color: Theme.of(context).backgroundColor.withOpacity(1),
        border: Border.all(
          color: Colors.white38,
        ),
      ),
      child: AspectRatio(
        aspectRatio: 1,
        child: Padding(
          padding: const EdgeInsets.all(4.0),
          child: Stack(
            children: [
              //To Show the participant Stream
              videoStream != null
                  ? RTCVideoView(
                      videoStream?.renderer as RTCVideoRenderer,
                      objectFit:
                          RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
                    )
                  : const Center(
                      child: Icon(
                        Icons.person,
                        size: 180.0,
                        color: Color.fromARGB(140, 255, 255, 255),
                      ),
                    ),

              //Display the Participant Name
              Positioned(
                bottom: 0,
                left: 0,
                child: FittedBox(
                  fit: BoxFit.scaleDown,
                  child: Container(
                    padding: const EdgeInsets.all(2.0),
                    decoration: BoxDecoration(
                      color: Theme.of(context).backgroundColor.withOpacity(0.2),
                      border: Border.all(
                        color: Colors.white24,
                      ),
                      borderRadius: BorderRadius.circular(4.0),
                    ),
                    child: Text(
                      widget.participant.displayName,
                      textAlign: TextAlign.center,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 10.0,
                      ),
                    ),
                  ),
                ),
              ),

              //Display the Participant Mic Status
              Positioned(
                  top: 0,
                  left: 0,
                  child: InkWell(
                    child: Container(
                      padding: const EdgeInsets.all(4),
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: audioStream != null
                            ? Theme.of(context).backgroundColor
                            : Colors.red,
                      ),
                      child: Icon(
                        audioStream != null ? Icons.mic : Icons.mic_off,
                        size: 16,
                      ),
                    )
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }

  //... _initListener

}

Enter fullscreen mode Exit fullscreen mode

Step 4:Run Your Code Now

$ flutter run

Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (1)

Collapse
 
muqeetwebdev profile image
Abdul Muqeet • Edited

By using Meet Hour Video Conference Flutter SDK you can build the video call app here is the SDK link: github.com/v-empower/MeetHour-Web-...