Build a Flutter Video Calling App in 4 Steps with Video SDK
Published: 2022-05-09 06:05:03

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

- Flutter SDK installed
- A token of VideoSDK from [VideoSDK Dashboard](https://app.videosdk.live/api-keys)

**Project Structure**

Create one flutter project first by writing the following command

<!--kg-card-begin: markdown-->
```js
$ flutter create videosdk_flutter_quickstart

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
root-Folder Name
   ├── ...
   ├── lib
   │ ├── join_screen.dart
        ├── main.dart
        ├── meeting_screen.dart
        ├── participant_grid_view.dart
        ├── participant_tile.dart

```
<!--kg-card-end: markdown-->
### Step 1:Flutter SDK Integration For Android

**1** : import [flutter sdk](https://pub.dev/packages/videosdk) from video SDK

<!--kg-card-begin: markdown-->
```js
$ flutter pub add videosdk

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

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
<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" />

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
android {
    //...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

```
<!--kg-card-end: markdown-->
- 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 **<project root>/ios/Runner/Info.plist** :

<!--kg-card-begin: markdown-->
```js
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>

```
<!--kg-card-end: markdown-->
### 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](https://app.videosdk.live/api-keys).

<!--kg-card-begin: markdown-->
```js
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: [

          ],
        ),
      ),
    );
  }

}

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
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",
        ),
      ),
    );
  }

}

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
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;
  }
}

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
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
    );
  }
}

```
<!--kg-card-end: markdown-->

**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.

<!--kg-card-begin: markdown-->
```js
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();
  }
}

```
<!--kg-card-end: markdown-->

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

We will pass the required parameters to the MeetingBuilder.

<!--kg-card-begin: markdown-->
```js
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) {

        },
      ),
    );
  }
}

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
class _MeetingScreenState extends State<MeetingScreen> {

  Meeting? meeting;

  Stream? videoStream;
  Stream? audioStream;

  //...build

}

```
<!--kg-card-end: markdown-->

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

<!--kg-card-begin: markdown-->
```js
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;
        });
      }
    });
  }

}

```
<!--kg-card-end: markdown-->
- We will be showing a Waiting to join screen until the meeting is joined and then show the meeting view.

<!--kg-card-begin: markdown-->
```js

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: [

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

}

```
<!--kg-card-end: markdown-->
- Inside our meeting view we will add a **ParticipantGrid** and three action buttons.

<!--kg-card-begin: markdown-->
```js
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"),
                    ),
                  ],
                )
              ],
            ),
          );
        }
      ),
    );
  }

}

```
<!--kg-card-end: markdown-->
- Add the **\_onWillPopScope()** which will handle the meeting leave on back button click.

<!--kg-card-begin: markdown-->
```js
class _MeetingScreenState extends State<MeetingScreen> {

  //...other declarations

  //...build method

  //... setMeetingListeners

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

```
<!--kg-card-end: markdown-->

**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**.

<!--kg-card-begin: markdown-->
```js
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;
        });
      },
    );
  }
}

```
<!--kg-card-end: markdown-->

**4** : Creating the **ParticipantTile** which will be placed inside the GridView and for that create **participant\_tile.dart** file

<!--kg-card-begin: markdown-->
```js
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();
  }

}

```
<!--kg-card-end: markdown-->
- Now we will initialise the state with the current streams of the Participant and add listeners for stream change

<!--kg-card-begin: markdown-->
```js

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

}

```
- 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.

```js
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

}

```

### Step 4:Run Your Code Now

```js
$ flutter run

```
<!--kg-card-end: markdown-->
#### 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](https://docs.videosdk.live/). If you face any difficulty with the implementation you can connect with us on our [discord community](https://discord.gg/Gpmj6eCq5u).