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, colleagues, and others with the help of online platforms to share content and knowledge and report to one another. VideoSDK 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.

5 Steps to Build a Video Calling App in Flutter

Develop and launch Apps for both Android and iOS at the same time.

Prerequisite

Project Structure

Create a new Flutter Video Call 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
         ├── room_controls.dart
         ├── room_screen.dart
         ├── participant_tile.dart

Step 1: Flutter Video Call SDK Integration

1: install videosdk package from the pub.dev

$ flutter pub add videosdk

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

$ flutter pub add http

Step 2: Setup for Android

1: 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.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />

<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 32 (currently default Flutter generator set it to 30).

Step 3: Setup for iOS

1: 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 4:Start Writing Your Code for Flutter Video Call App

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

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

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

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

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

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

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

  • Create Room Button - This button will create a new room for you.
  • TextField for RoomId - This text field will contain the RoomId you want to join.
  • Join Button - This button will join the room that you provided.

JoinScreen will accept 3 functions in the constructor.

  • onCreateRoomButtonPressed - invoked when the Create Room button pressed
  • onJoinRoomButtonPressed - invoked when the Join button pressed
  • onRoomIdChanged - invoked when RoomId TextField value changed

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

import 'package:flutter/material.dart';

class JoinScreen extends StatelessWidget {
  final void Function() onCreateRoomButtonPressed;
  final void Function() onJoinRoomButtonPressed;
  final void Function(String) onRoomIdChanged;

  const JoinScreen({
    Key? key,
    required this.onCreateRoomButtonPressed,
    required this.onJoinRoomButtonPressed,
    required this.onRoomIdChanged,
  }) : super(key: key);

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

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

Create a new room_controls.dart file with a stateless widget named RoomControls.

The RoomControls will consist of:

  • Leave Button - This button will leave the room.
  • Toggle Mic Button - This button will enable or disable the mic.
  • Toggle Camera Button - This button will enable or disable the camera.

RoomControls will accept 3 functions in the constructor

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

class RoomControls extends StatelessWidget {
  final void Function() onToggleMicButtonPressed;
  final void Function() onToggleCameraButtonPressed;
  final void Function() onLeaveButtonPressed;

  const RoomControls({
    Key? key,
    required this.onToggleMicButtonPressed,
    required this.onToggleCameraButtonPressed,
    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 Camera"),
          onPressed: onToggleCameraButtonPressed,
        )
      ],
    );
  }
}

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

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

The ParticipantTile will consist of:

  • RTCVideoView - This will show a remote participant video stream.

ParticipantTile will accept Stream in the 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 RoomScreen

For that, create room_screen.dart file and create RoomScreen as a StateFul Widget.

RoomScreen will accept roomId and token in the constructor

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

class RoomScreen extends StatefulWidget {
  final String roomId;
  final String token;
  final void Function() leaveRoom;

  const RoomScreen(
      {Key? key,
      required this.roomId,
      required this.token,
      required this.leaveRoom})
      : super(key: key);

  @override
  State<RoomScreen> createState() => _RoomScreenState();
}

class _RoomScreenState extends State<RoomScreen> {
  Map<String, Stream?> participantVideoStreams = {};

  bool micEnabled = true;
  bool camEnabled = true;
  late Room room;

  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 setRoomEventListener(Room _room) {
    setParticipantStreamEvents(_room.localParticipant);
    _room.on(
      Events.participantJoined,
      (Participant participant) => setParticipantStreamEvents(participant),
    );
    _room.on(Events.participantLeft, (String participantId) {
      if (participantVideoStreams.containsKey(participantId)) {
        setState(() => participantVideoStreams.remove(participantId));
      }
    });
    _room.on(Events.roomLeft, () {
      participantVideoStreams.clear();
      widget.leaveRoom();
    });
  }
  
   @override
  void initState() {
    super.initState();
    // Create instance of Roo
    
    room = VideoSDK.createRoom(
      roomId: widget.roomId,
      token: widget.token,
      displayName: "Yash Chudasama",
      micEnabled: micEnabled,
      camEnabled: camEnabled,
      maxResolution: 'hd',
      defaultCameraIndex: 1,
      notification: const NotificationInfo(
        title: "Video SDK",
        message: "Video SDK is sharing screen in the room",
        icon: "notification_share", // drawable icon name
      ),
    );

    setRoomEventListener(room);

    // Join room
    room.join();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text("Room ID: ${widget.roomId}"),
          RoomControls(
            onToggleMicButtonPressed: () {
              micEnabled ? room.muteMic() : room.unmuteMic();
              micEnabled = !micEnabled;
            },
            onToggleCameraButtonPressed: () {
              cameraEnabled
                  ? room.disableCamera()
                  : room.enableCamera();
              cameraEnabled = !cameraEnabled;
            },
            onLeaveButtonPressed: () => room.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 roomScreen will get appear.

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

VideoSDKQuickStart widget will return RoomScreen if the room is active, otherwise, return JoinScreen.

import 'package:flutter/material.dart';
import 'api.dart';
import 'join_screen.dart';
import 'room_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 roomId = "";
  bool isRoomActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("VideoSDK QuickStart"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: isRoomActive
            ? RoomScreen(
                roomId: roomId,
                token: token,
                leaveRoom: () {
                  setState(() => isRoomActive = false);
                },
              )
            : JoinScreen(
                onRoomIdChanged: (value) => roomId = value,
                onCreateRoomButtonPressed: () async {
                  roomId = await createRoom();
                  setState(() => isRoomActive = true);
                },
                onJoinRoomButtonPressed: () {
                  setState(() => isRoomActive = true);
                },
              ),
      ),
    );
  }
}

Step 5:Run Your Code Now

$ flutter run
Video SDK Image

Conclusion

With this, we successfully built the flutter video call with VideoSDK. 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.

More Flutter Resources