Flutter video calling implementation by awesome Agora SDK

Might be Wherever you are in this digital world, Video calling is the only channel that does not make you feel isolated in connecting with your friends and family.

In Flutter, we can enable real-time audio and video calling features by using Agora RTC (Real-time Communications) SDK. Apart from basic communications, this SDK provides advanced functionalities such as screen sharing, audio mixing, modifying raw data… about Agora SDK refer link.

In this article, we are going to learn about the implementation of the video calling feature in Flutter apps.

These are the following modules we are going to implement in this article
1. join/leave a channel
2. Mute/unmute
3. Switching camera
4. Multiple video views Layout

Final Output:

ezgif.com video to gif2 1

Let’s start with creating a project in Agora’s developer console

1. First thing first, we have to create an account in Agora’s dashboard
2. Once Account registration has done, we need to create a project, Go to Project Management in the left Navigation menu and click on create button.

agora ps 1 1

3. We will get a pop-up window to take the project name and authentication mechanism type (It’s recommended to select Secure mode: APP ID + Token) and finally click on submit.

agora ps 2 2

4. Get an App ID: Copy the App ID, which is a unique identifier and assigned automatically for every project

agora ps 3

5. Generating a temporary token:

  • Agora uses a token to authenticate the users to join a channel for communication security.
  • For this, we need to click on the key icon on the Project management page
agora ps key
  • It will navigate to another page to take channel as an Input and after we need to click on Generate Temp Token button for generating the token.
agora ps 4

When joining, the entered channel name should be the same as with we used to generate the Temporary token. Here we created a Temporary token for the testing purpose it will be valid only for 24 hrs. For production, we have to use our own server to generate tokens. Reference Link.

As of now, we completed the agora’s project setup.

Create a Flutter Project

  1.  Create a new Flutter project in your IDE. If you want to know the creation of a new Flutter project you can refer to this creating new flutter project.
  2. Once project creation has done then add the latest versions of agora_rtc_engine, permission_handler, and provider dependencies to our pubspec.yaml file
 dependencies:
  flutter:
    sdk: flutter
  agora_rtc_engine: <"latest version">
  permission_handler: <"latest version">
  provider: <"latest version">
  • agora_rtc_engine: This plugin is a wrapper for Agora Video SDK which is to enable real-time communications in our own application quickly. For more information refer to the link.
  • permission_handler: This plugin provides a cross-platform (iOS, Android) API to request permissions and check their status. In our app, we need to get the microphone and camera permissions that will be used during the video call. For more information refer to the link
  • provider: is one of the famous package for providing dependency injection and state management for flutter apps. For more information refer to the link.

Implementation

Project Structure
The following Image shows our code structure under the lib

Screenshot 32
  1. Create a home page :
    Let’s Create a dart file named home_page.dart, which takes the channel name as Input and navigate to calling screen whenever user click on the join button.
//home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'home_model.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  /// create a channelController to retrieve text value
  final _channelController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, HomeNotifier homeNotifier, child) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Video Call App'),
        ),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.only(left: 16, right: 16),
            child: Container(
              height: 150,
              child: Column(
                children: <Widget>[
                  TextField(
                    controller: _channelController,
                    decoration: InputDecoration(
                      errorText: homeNotifier.validateError
                          ? 'Channel name is mandatory'
                          : null,
                      border: UnderlineInputBorder(
                        borderSide: BorderSide(width: 1),
                      ),
                      hintText: 'Channel name',
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(vertical: 20),
                    child: Row(
                      children: [
                        Expanded(
                          child: MaterialButton(
                            onPressed: () => {
                              homeNotifier.onJoin(
                                  context, _channelController.text),
                            },
                            child: Text('Join'),
                            color: Colors.teal,
                            textColor: Colors.white,
                          ),
                        ),
                      ],
                    ),
                  )
                ],
              ),
            ),
          ),
        ),
      );
    });
  }

  @override
  void dispose() {
    _channelController.dispose();
    super.dispose();
  }
}

2. Create a model class for the Home page
Since we using a provider for state management, creating a model class named home_model.dart.

//home_model.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_video_call_app/screens/call/call_screen.dart';
import 'package:permission_handler/permission_handler.dart';

class HomeNotifier extends ChangeNotifier {
  bool _validateError = false;

  bool get validateError => _validateError;

  setValue(bool value) {
    _validateError = value;
    notifyListeners();
  }

  Future<void> onJoin(BuildContext context, String chanelName) async {
    if (chanelName.isEmpty) {
      setValue(true);
    } else {
      setValue(false);
      // await for camera and mic permissions before pushing video page
      await _handleCameraAndMic(Permission.camera);
      await _handleCameraAndMic(Permission.microphone);
      await Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => CallScreen(
                    channelName: chanelName,
                  )));
    }
  }

  Future<void> _handleCameraAndMic(Permission permission) async {
    final status = await permission.request();
    print("Permissions status: $status");
  }
}
  • _validateError: gives the boolean value whether the user entered the channel or not
  • onJoin(…): This method will be called when the user clicks on the join button on the home page. Which is used to pass the channel name to the CallScreen page after taking the camera and mic permissions from the user.
  • _handleCameraAndMic(…): This method requests the permissions.

3. Create a utils class
As we discussed before, we need AppID and Token to link our app to the Agora Rtc engine and joining to a particular channel. Here I have kept my AppID and Token in utils.dart class

const APP_ID =  "your AppID"
const Token = "Your Token"

4. Create a calling page
Now, Create a dart file named call_screen.dart, which having all functionalities such as video view, toggle mute, switching camera, end call, and showing log on the screen.

//call_screen.dart
import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
import 'package:flutter/material.dart';
import 'package:flutter_video_call_app/screens/base_widget.dart';
import 'package:flutter_video_call_app/screens/call/call_model.dart';

class CallScreen extends StatefulWidget {
  final String channelName;

  const CallScreen({Key key, this.channelName}) : super(key: key);

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

class _CallScreenState extends State<CallScreen> {
  /// Helper function to get list of native views
  List<Widget> _getRenderViews(CallNotifier model) {
     ....
  }

  /// Video view wrapper
  Widget _videoView(view) {
    return Expanded(child: Container(child: view));
  }

  /// Video view row wrapper
  Widget _expandedVideoRow(List<Widget> views) {
     ....
  }

  /// Video layout wrapper
  Widget _viewRows(CallNotifier notifier) {
     ....
  }

  /// Toolbar layout
  Widget _toolbar(CallNotifier notifier) {
     ....
  }

  /// Info panel to show logs
  Widget _panel(CallNotifier notifier) {
    return Container( ... );
  }

  void _onCallEnd(BuildContext context) {
     ....
  }

  void _onToggleMute(CallNotifier notifier) {
     ....
  }

  void _onSwitchCamera(CallNotifier model) {
     ....
  }

  @override
  Widget build(BuildContext context) {
    return BaseWidget<CallNotifier>(
      model: CallNotifier(),
      onModelReady: (model) => model.init(widget.channelName),
      builder: (context, notifier, child) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Video Call'),
          ),
          backgroundColor: Colors.black,
          body: Center(
            child: Stack(
              children: <Widget>[
                _viewRows(notifier),
                _panel(notifier),
                _toolbar(notifier),
              ],
            ),
          ),
        );
      },
    );
  }
}
  • _viewRows(…): the widget gives the Layout based on the number of users who joined in.
  • _panel(…): the widget prints the channel logs on the screen.
  • _toolbar(…): this widget gives interactive options like mute/unmute the microphone, switching cameras, and ending the call.
  • _onToggleMute(…): the method is used for muting or unmuting microphone
  • _onSwitchCamera(…): the method is used to switching the cameras.
  • _onCallEnd(..): to disconnect the call

5. Create a model class for the Call screen
Creating a model class named call_model.dart.

//call_model.dart. 
import 'package:agora_rtc_engine/rtc_engine.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_video_call_app/utils/utils.dart';

class CallNotifier extends ChangeNotifier {
  var _users = <int>[];
  var _infoStrings = <String>[];
  bool _isMuted = false;
  RtcEngine _engine;

  RtcEngine get engine => _engine;

  set setEngine(RtcEngine engine) {
    _engine = engine;
    notifyListeners();
  }

  bool get isMuted => _isMuted;

  set isMuted(value) {
    _isMuted = !value;
    notifyListeners();
  }

  get users => _users;

  addUser(value) {
    _users.add(value);
    notifyListeners();
  }

  get infoStrings => _infoStrings;

  infoString(String value) {
    _infoStrings.add(value);
    notifyListeners();
  }

  removeUser(value) {
    _users.remove(value);
    notifyListeners();
  }
  
  clearUsers() {
    _users.clear();
    notifyListeners();
  }
  
  Future<void> init(String channelName) async {
    if (APP_ID.isEmpty) {
      infoString('APP_ID missing, please provide your APP_ID in settings.dart');
      infoString('Agora Engine is not starting');
      return;
    }
    await _initAgoraRtcEngine();
    _addAgoraEventHandlers();
    await _engine.enableLocalVideo(true);
    VideoEncoderConfiguration configuration = VideoEncoderConfiguration();
    configuration.dimensions = VideoDimensions(1920, 1080);
    await _engine.setVideoEncoderConfiguration(configuration);
    await _engine.joinChannel(Token, channelName, null, 0);
  }

  /// Create agora sdk instance and initialize
  Future<void> _initAgoraRtcEngine() async {
    _engine = await RtcEngine.create(APP_ID);
    setEngine = _engine;
    await _engine.enableVideo();
    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
    await _engine.setClientRole(ClientRole.Broadcaster);
  }

  void _addAgoraEventHandlers() {
    _engine.setEventHandler(RtcEngineEventHandler(error: (code) {
      infoString('onError: $code');
    }, joinChannelSuccess: (channel, uid, elapsed) {
      infoString('onJoinChannel: $channel, uid: $uid');
    }, leaveChannel: (stats) {
      infoString('onLeaveChannel');
      _users.clear();
    }, userJoined: (uid, elapsed) {
      infoString('userJoined: $uid');
      addUser(uid);
    }, userOffline: (uid, elapsed) {
      infoString('userOffline: $uid');
      removeUser(uid);
    }, firstRemoteVideoFrame: (uid, width, height, elapsed) {
    }));
  }

  @override
  void dispose() {
    clearUsers();
    _engine.destroy();
    _engine.leaveChannel();
    super.dispose();
  }
}
  • _users: The list contains the uid of the users who joined the channel
  • _infoStrings: List contains the logs of the call.
  • _isMuted: gives the boolean value whether the microphone is muted or not
  • _engine: Is a reference variable of RtcEngine.
  • init(..): This method will call automatically whenever a user enters the call screen. if the AppID is not empty agora Rtc engine will be initialized by calling the _initAgoraRtcEngine() method, call events will be added to our log by calling the _addAgoraEventHandlers() method and some video configurations also added
  • _initAgoraRtcEngine(): AgoraRtcEngine will be initialized and our app will links to Agora’s engine by using AppID.
  • _addAgoraEventHandlers(): This method is used to add an event handler so that we can get uid of users who joined in and we can build the logs based on the events.
  • _engine.joinChannel(Token, channelName, null, 0): It allows the user to join a particular channel based on channel name and Token.
  • dispose(): Finally, we have to destroy the RtcEngine in this method.

In this article, we referred to most of the code from Agora’s repo link and tried to add state management to that code using the provider.

Thanks for reading..!!! 🙂

1 thought on “Flutter video calling implementation by awesome Agora SDK”

Leave a Reply

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