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

Do you marvel what video calling apps really imply? If not then no worries, you’ll get an thought quickly whereas studying this weblog. All of us have come throughout digital phrases extra typically these days. We’re connecting with our workers, colleague, and others with the assistance of on-line platforms to share content material, data and to report to at least one one other. Video SDK got here up with the thought of creating an app that helps individuals with connecting remotely. Throughout the assembly one can current their content material to others, can increase their question by dropping a textual content, one can ask questions by turning on the mic and lots of extra options are there you’ll get acquaintance of on the finish of this weblog.



4 Steps to Construct a Video Calling App in Flutter

  • Develop and launch in each Android and iOS on the similar time.

Prerequisite

Venture Construction

Create one flutter challenge first by writing the next command

$ flutter create videosdk_flutter_quickstart

Enter fullscreen mode

Exit fullscreen mode

Your challenge construction’s lib listing must be as similar as talked about under

root-Folder Title
   ├── ...
   ├── lib
    ├── join_screen.dart
        ├── foremost.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 so as to add http library to carry out community name to generate meetingId
$ flutter pub add http

Enter fullscreen mode

Exit fullscreen mode

2 : Replace the AndroidManifest.xml for the permissions we will likely be utilizing to implement the audio and video options. You could find the AndroidManifest.xml file at /android/app/src/foremost/AndroidManifest.xml

<makes use of-characteristic android:title="android.{hardware}.digicam" />
<makes use of-characteristic android:title="android.{hardware}.digicam.autofocus" />
<makes use of-permission android:title="android.permission.CAMERA" />
<makes use of-permission android:title="android.permission.RECORD_AUDIO" />
<makes use of-permission android:title="android.permission.ACCESS_NETWORK_STATE" />
<makes use of-permission android:title="android.permission.CHANGE_NETWORK_STATE" />
<makes use of-permission android:title="android.permission.MODIFY_AUDIO_SETTINGS" />

Enter fullscreen mode

Exit fullscreen mode

3 : Additionally you have to to set your construct settings to Java 8 as a result of the official WebRTC jar now makes use of static strategies in EglBase interface. Simply add this to your app-level construct.gradle

android {
    //...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

Enter fullscreen mode

Exit fullscreen mode

  • If mandatory, in the identical construct.gradle you have to to extend minSdkVersion of defaultConfig as much as 21 (at the moment default Flutter generator set it to 16 ).
  • If mandatory, in the identical construct.gradle you have to to extend compileSdkVersion and targetSdkVersion as much as 31 (at the moment default Flutter generator set it to 30 ).



Step 2:Flutter SDK Integration For IoS

Add the next entries which permit your app to entry the digicam and microphone to your Information.plist file, situated in /ios/Runner/Information.plist :

<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Digital camera Utilization!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Utilization!</string>

Enter fullscreen mode

Exit fullscreen mode



Step 3:Begin Writing Your Code

1 : Let’s create a join-screen now.The Becoming a member of display screen will encompass

  • Create Button – This button will create a brand new assembly for you.
  • TextField for Assembly ID – This textual content discipline will comprise the assembly ID you wish to be part of.
  • Be a part of Button – This button will be part of the assembly which you offered.

Create a brand new dart file join_screen.dart which is able to comprise our Stateful Widget named JoinScreen.

Exchange the _token with the pattern token you generated from the VideoSDK Dashboard.

import 'dart:convert';

import 'bundle:flutter/materials.dart';
import 'bundle:http/http.dart' as http;
import 'bundle:videosdk_flutter_quickstart/meeting_screen.dart';

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

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

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

  String _meetingID = "";

  @override
  Widget construct(BuildContext context) {
    remaining ButtonStyle _buttonStyle = TextButton.styleFrom(
      main: Colours.white,
      backgroundColor: Theme.of(context).primaryColor,
      textStyle: const TextStyle(
        fontWeight: FontWeight.daring,
      ),
    );
    return Scaffold(
      appBar: AppBar(
        title: const Textual content("VideoSDK RTC"),
      ),
      backgroundColor: Theme.of(context).backgroundColor,
      physique: SafeArea(
        youngster: Column(
          mainAxisAlignment: MainAxisAlignment.middle,
          kids: [

          ],
        ),
      ),
    );
  }

}

Enter fullscreen mode

Exit fullscreen mode

Replace the JoinScreen with two buttons and a textual content discipline within the kids property of column widget.

class _JoinScreenState extends State<JoinScreen> {
  ...
  @override
  Widget construct(BuildContext context) {
    ...
    return Scaffold(
      ...
      physique: SafeArea(
        youngster: Column(
          mainAxisAlignment: MainAxisAlignment.middle,
          kids: [
            //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"),
            )
          ],
        ),
      ),
    );
  }

  //That is technique known as 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() within the JoinScreen which is able to generate a brand new assembly id.

class _JoinScreenState extends State<JoinScreen> {

  //...different variables

  //...construct technique

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

    remaining meetingId = json.decode(meetingIdResponse.physique)['meetingId'];
    return meetingId;
  }
}

Enter fullscreen mode

Exit fullscreen mode

Make the JoinScreen as your property within the foremost.dart as proven under.

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

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

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

Enter fullscreen mode

Exit fullscreen mode

2 : We’re achieved with the creation of be part of display screen , now let’s create a gathering display screen of video calling app.

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

MeetingScreen accepts the next parameter:

  • meetingId : This would be the assembly Id we are going to becoming a member of
  • token : Auth Token to configure the Assembly
  • displayName : Title with which the participant will likely be joined
  • micEnabled : (Optionally available) If true, the mic will likely be on while you be part of the assembly else will probably be off.
  • webcamEnabled : (Optionally available) If true, the webcam will likely be on while you be part of the assembly else will probably be off.
import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';
import 'bundle:videosdk_flutter_quickstart/join_screen.dart';
import 'bundle:videosdk_flutter_quickstart/participant_grid_view.dart';

class MeetingScreen extends StatefulWidget {

  //add the next parameters to your MeetingScreen
  remaining String meetingId, token, displayName;
  remaining bool micEnabled, webcamEnabled;
  const MeetingScreen({
    Key? key,
    required this.meetingId,
    required this.token,
    required this.displayName,
    this.micEnabled = true,
    this.webcamEnabled = true
  }) : tremendous(key: key);

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

class _MeetingScreenState extends State<MeetingScreen> {

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

Enter fullscreen mode

Exit fullscreen mode

Now we are going to replace _MeetingScreenState to make use of the MeetingBuilder to create our assembly.

We are going to move the required parameters to the MeetingBuilder.

class _MeetingScreenState extends State<MeetingScreen> {

  @override
  Widget construct(BuildContext context) {
    return WillPopScope(
      onWillPop: _onWillPopScope,
      //MeetingBuilder is a category of @videosdk/rtc.dart
      youngster: 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 display screen within the assembly",
          icon: "notification_share", // drawable icon title
        ),
        builder: (_meeting) {

        },
      ),
    );
  }
}

Enter fullscreen mode

Exit fullscreen mode

Now we are going to add assembly , videoStream , audioStream which is able to retailer the assembly and the streams for native participant.

class _MeetingScreenState extends State<MeetingScreen> {

  Assembly? assembly;

  Stream? videoStream;
  Stream? audioStream;

  //...construct

}

Enter fullscreen mode

Exit fullscreen mode

Now we are going to replace the builder to generate a gathering view.

  • Including the Occasion listeners for the assembly and setting the state of our native assembly
class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

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

        builder: (_meeting) {
          // Referred to as when joined in assembly
          _meeting.on(
            Occasions.meetingJoined,
            () {
              setState(() {
                assembly = _meeting;
              });

              // Setting assembly occasion listeners
              setMeetingListeners(_meeting);
            },
          );
        }
      ),
    );
  }

  void setMeetingListeners(Assembly assembly) {
    // Referred to as when assembly is ended
    assembly.on(Occasions.meetingLeft, () {
      Navigator.pushAndRemoveUntil(
          context,
          MaterialPageRoute(builder: (context) => const JoinScreen()),
          (route) => false);
    });

    // Referred to as when stream is enabled
    assembly.localParticipant.on(Occasions.streamEnabled, (Stream _stream) {
      if (_stream.type == 'video') {
        setState(() {
          videoStream = _stream;
        });
      } else if (_stream.type == 'audio') {
        setState(() {
          audioStream = _stream;
        });
      }
    });

    // Referred to as when stream is disabled
    assembly.localParticipant.on(Occasions.streamDisabled, (Stream _stream) {
      if (_stream.type == 'video' && videoStream?.id == _stream.id) {
        setState(() {
          videoStream = null;
        });
      } else if (_stream.type == 'audio' && audioStream?.id == _stream.id) {
        setState(() {
          audioStream = null;
        });
      }
    });
  }

}

Enter fullscreen mode

Exit fullscreen mode

  • We will likely be exhibiting a Ready to affix display screen till the assembly is joined after which present the assembly view.

class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

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

        builder: (_meeting) {
          //_meeting listener

          // Exhibiting ready display screen
          if (assembly == null) {
            return Scaffold(
              physique: Middle(
                youngster: Column(
                  mainAxisSize: MainAxisSize.min,
                  kids: [
                    const CircularProgressIndicator(),
                    SizedBox(height: 20),
                    const Text("waiting to join meeting"),
                  ],
                ),
              ),
            );
          }

          //Assembly View
          return Scaffold(
            backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
            appBar: AppBar(
              title: Textual content(widget.meetingId),
            ),
            physique: Column(
              kids: [

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

}

Enter fullscreen mode

Exit fullscreen mode

  • Inside our assembly view we are going to add a ParticipantGrid and three motion buttons.
class _MeetingScreenState extends State<MeetingScreen> {

  //...state declartations

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

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

          //...Ready Display screen UI

          //Assembly View
          return Scaffold(
            backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
            appBar: AppBar(
              title: Textual content(widget.meetingId),
            ),
            physique: Column(
              kids: [
                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 is able to deal with the assembly depart on again button click on.
class _MeetingScreenState extends State<MeetingScreen> {

  //...different declarations

  //...construct technique

  //... setMeetingListeners

  Future<bool> _onWillPopScope() async {
    assembly?.depart();
    return true;
  }
}

Enter fullscreen mode

Exit fullscreen mode

3 : Subsequent we will likely be creating the ParticipantGridView which will likely be used to point out the participant’s view.

ParticipantGridView maps every participant with a ParticipantTile.

It updates the participant’s listing every time somebody leaves or joins the assembly utilizing the participantJoined and participantLeft occasion listeners on the assembly.

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

import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';

import 'participant_tile.dart';

class ParticipantGridView extends StatefulWidget {
  remaining Assembly assembly;
  const ParticipantGridView({
    Key? key,
    required this.assembly,
  }) : tremendous(key: key);

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

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

  @override
  void initState() {
    // Initialize contributors
    localParticipant = widget.assembly.localParticipant;
    contributors = widget.assembly.contributors;

    // Setting assembly occasion listeners
    setMeetingListeners(widget.assembly);
    tremendous.initState();
  }

  @override
  Widget construct(BuildContext context) {
    return GridView.rely(
      crossAxisCount: 2,
      kids: [
        //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(Assembly _meeting) {
    // Referred to as when participant joined assembly
    _meeting.on(
      Occasions.participantJoined,
      (Participant participant) {
        remaining newParticipants = contributors;
        newParticipants[participant.id] = participant;
        setState(() {
          contributors = newParticipants;
        });
      },
    );

    // Referred to as when participant left assembly
    _meeting.on(
      Occasions.participantLeft,
      (participantId) {
        remaining newParticipants = contributors;

        newParticipants.take away(participantId);
        setState(() {
          contributors = newParticipants;
        });
      },
    );
  }
}

Enter fullscreen mode

Exit fullscreen mode

4 : Creating the ParticipantTile which will likely be positioned contained in the GridView and for that create participant_tile.dart file

import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';

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

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

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

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

}

Enter fullscreen mode

Exit fullscreen mode

  • Now we are going to initialise the state with the present streams of the Participant and add listeners for stream change

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

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

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

  //... construct

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

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

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

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

}

Enter fullscreen mode

Exit fullscreen mode

  • Now we are going to create RTCVideoView to point out the participant stream and in addition add different elements just like the title and mic standing indicator of the participant.
class _ParticipantTileState extends State<ParticipantTile> {
  Stream? videoStream;
  Stream? audioStream;

  @override
  Widget construct(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(4.0),
      ornament: BoxDecoration(
        colour: Theme.of(context).backgroundColor.withOpacity(1),
        border: Border.all(
          colour: Colours.white38,
        ),
      ),
      youngster: AspectRatio(
        aspectRatio: 1,
        youngster: Padding(
          padding: const EdgeInsets.all(4.0),
          youngster: Stack(
            kids: [
              //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 efficiently constructed the video chat app utilizing the video SDK in Flutter. If you happen to want to add functionalities like chat messaging, and display screen sharing, you may all the time take a look at our documentation. If you happen to face any problem with the implementation you may join with us on our discord community.

Add a Comment

Your email address will not be published. Required fields are marked *