Try Live Demo

Video conferencing is a type of virtual, online meeting where two or more people talk through a video and audio call in real time. With the pandemic, the culture of remote work has become widespread. As a result of that meeting online is very popular nowadays. Did you know that, you can easily develop your own conference application?

Here is a quick tutorial to create a conference solution easily on Flutter.

Before we start on how to make a video conferencing flutter app, l wanted to give some information about Ant Media Server’s Flutter SDK and Conference Solution.

Ant Media’s Flutter WebRTC SDK lets you build your own Flutter application that can publish and play WebRTC broadcasts with just a few lines of code. Besides, you can also benefit from Flutter’s multi-platform features, and build not only mobile apps but Mac OS and Windows apps as well.

You can get complete documentation of Flutter SDK from here

When we check Ant Media’s Conference solution, it supports both MCU and SFU topology. Besides that, it’s really easy to create or manage conference rooms through their Rest API.

You can get more information about Conference Solution from here

How to Build a Video Conferencing App with Flutter?

Prerequisites to build video conferencing app with Flutter

Let’s make sure you have the following in your development environment:

For iOS

For Android

Let’s get started building a video conferencing app

Create a Flutter project

On Terminal:

				
					flutter create --org com.yourdomain my_app

cd my_app
				
			

On Android Studio:

1. Open the IDE and select New Flutter Project.
2. Select Flutter, and verify the Flutter SDK path with the SDK’s location. Then click Next.
3. Enter a project name (for example, my_app).
4. Select Application as the project type. Then click Next.
5. Click Finish.
6. Wait for Android Studio to create the project.

Add Dependencies:

Open pubspec.yml and add  ant_media_flutter: ^0.0.8 as dependency.

Go to the lib folder

				
					cd lib
				
			

Add following code inside main.dart

				
					import 'dart:core';
import 'dart:io';

import 'package:ant_media_flutter/ant_media_flutter.dart';
import 'package:flutter/material.dart';
import 'package:conference/conference.dart';
import 'package:conference/route_item.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:get/get.dart';

void main() => runApp(const MaterialApp(
      home: MyApp(),
      debugShowCheckedModeBanner: false,
    ));

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

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

enum DialogDemoAction {
  cancel,
  connect,
}

class _MyAppState extends State<MyApp> {
  List<RouteItem> items = [];
  String _server = 'wss://ovh36.antmedia.io:5443/LiveApp/websocket';
  late SharedPreferences _prefs;
  String _streamId = 'rfrfrf';
  String _roomId = 'room1';
  final navigatorKey = GlobalKey<NavigatorState>();

  final _streamidcontroller = TextEditingController();
  final _roomidcontroller = TextEditingController();

  @override
  initState() {
    super.initState();
    _initItems();
    AntMediaFlutter.requestPermissions();

    if (Platform.isAndroid) {
      AntMediaFlutter.startForegroundService();
    }
  }

  _buildRow(context, item) {
    return ListBody(children: <Widget>[
      ListTile(
        title: Text(item.title),
        onTap: () => item.push(context),
        trailing: const Icon(Icons.arrow_right),
      ),
      const Divider()
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      navigatorKey: navigatorKey,
      home: Scaffold(
          appBar: AppBar(
            title: const Text('Ant Media Server Example'),
            actions: <Widget>[
              IconButton(
                icon: const Icon(Icons.settings),
                onPressed: () {
                  _showServerAddressDialog(context);
                },
                tooltip: 'setup',
              ),
            ],
          ),
          body: ListView.builder(
              shrinkWrap: true,
              padding: const EdgeInsets.all(0.0),
              itemCount: items.length,
              itemBuilder: (context, i) {
                return _buildRow(context, items[i]);
              })),
    );
  }

  void showStreamIdDialog<T>(
      {required BuildContext context, required Widget child}) {
    showDialog<T>(
      context: context,
      builder: (BuildContext context) => child,
    ).then<void>((T? value) {
      // The value passed to Navigator.pop() or null.
      if (value != null) {
        if (value == DialogDemoAction.connect) {
          String? settedIP = _prefs.getString('server');
          _prefs.setString('streamId', _streamId);
          _prefs.setString('roomId', _roomId);
          if (settedIP != null) {
              Navigator.push(
        context,
        MaterialPageRoute(
            builder: (BuildContext context) => Conference(
                  ip: settedIP,
                  id: _streamId,
                  userscreen: false,
                  roomId: _roomId,
                )));
          }
        }
      }
    });
  }

  void showServerAddressDialog<T>(
      {required BuildContext context, required Widget child}) {
    showDialog<T>(
      context: context,
      builder: (BuildContext context) => child,
    ).then<void>((T? value) {
      // The value passed to Navigator.pop() or null.
    });
  }

  void _showToastServer(BuildContext context) {
    if (_server == '') {
      Get.snackbar('Warning', 'Set the server address first',
          barBlur: 1,
          backgroundColor: Colors.redAccent,
          overlayBlur: 1,
          animationDuration: const Duration(milliseconds: 500),
          duration: const Duration(seconds: 2));
    } else if (_server != '') {
      Get.snackbar('Success!', 'Server Address has been set successfully',
          barBlur: 1,
          backgroundColor: Colors.greenAccent,
          overlayBlur: 1,
          animationDuration: const Duration(milliseconds: 500),
          duration: const Duration(seconds: 2));
    }
  }

  void _showToastStream(BuildContext context) {
    if (_streamId == '' || _streamId == 'Enter stream id') {
      Get.snackbar('Warning', 'Set the stream id',
          barBlur: 1,
          backgroundColor: Colors.redAccent,
          overlayBlur: 1,
          animationDuration: const Duration(milliseconds: 500),
          duration: const Duration(seconds: 2));
    }
  }

  void _showToastRoom(BuildContext context) {
    if (_roomId == '' || _roomId == 'Enter room id') {
      Get.snackbar('Warning', 'Set the room id',
          barBlur: 1,
          backgroundColor: Colors.redAccent,
          overlayBlur: 1,
          animationDuration: const Duration(milliseconds: 500),
          duration: const Duration(seconds: 2));
    }
  }

  _showStreamIdDialog(context) {
    if (_server == '') {
      _showToastServer(context);
    } else {
      showStreamIdDialog<DialogDemoAction>(
          context: context,
          child: AlertDialog(
              content: SizedBox(
                  height: 160,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      const SizedBox(
                        height: 20,
                      ),
                      const Text(
                        'Enter stream id',
                        textAlign: TextAlign.left,
                      ),
                      const SizedBox(
                        height: 0,
                      ),
                      TextField(
                        textAlign: TextAlign.start,
                        onChanged: (String text) {
                          setState(() {
                            _streamId = text;
                          });
                        },
                        controller: _streamidcontroller,
                        decoration: InputDecoration(
                          hintText: _streamId,
                          suffixIcon: IconButton(
                            onPressed: () => _streamidcontroller.clear(),
                            icon: const Icon(Icons.clear),
                          ),
                        ),
                      ),
                      const SizedBox(
                        height: 10,
                      ),
                      const Text('Enter room id'),
                      TextField(
                        onChanged: (String text) {
                          setState(() {
                            _roomId = text;
                          });
                        },
                        controller: _roomidcontroller,
                        decoration: InputDecoration(
                          hintText: _roomId,
                          suffixIcon: IconButton(
                            onPressed: () => _roomidcontroller.clear(),
                            icon: const Icon(Icons.clear),
                          ),
                        ),
                        textAlign: TextAlign.start,
                      ),
                    ],
                  )),
              actions: <Widget>[
                MaterialButton(
                    child: const Text('Cancel'),
                    onPressed: () {
                      Navigator.of(context, rootNavigator: true)
                          .pop(DialogDemoAction.cancel);
                    }),
                MaterialButton(
                    child: const Text('Connect'),
                    onPressed: () {
                      if (_streamId == '' || _streamId == 'Enter stream id') {
                        _showToastStream(context);
                      } else if (_roomId == '' || _roomId == 'Enter room id') {
                        _showToastRoom(context);
                      } else {
                        Navigator.of(context, rootNavigator: true)
                            .pop(DialogDemoAction.connect);
                      }
                    }),
              ]));
    }
  }

  void _showServerAddressDialog(BuildContext context) {
    var _controller = TextEditingController();
    //final context = navigatorKey.currentState?.overlay?.context;
    showServerAddressDialog<DialogDemoAction>(
        context: context,
        child: AlertDialog(
            title: const Text(
                'Enter Stream Address using the following format:\n wss://domain:port/WebRTCAppEE/websocket'),
            content: TextField(
              onChanged: (String text) {
                setState(() {
                  _server = text;
                });
              },
              controller: _controller,
              decoration: InputDecoration(
                hintText: _server == ''
                    ? 'wss://domain:port/WebRTCAppEE/websocket'
                    : _server,
                suffixIcon: IconButton(
                  onPressed: () => _controller.clear(),
                  icon: const Icon(Icons.clear),
                ),
              ),
              textAlign: TextAlign.center,
            ),
            actions: <Widget>[
              MaterialButton(
                  child: const Text('Cancel'),
                  onPressed: () {
                    Navigator.pop(context, DialogDemoAction.cancel);
                  }),
              MaterialButton(
                  child: const Text('Set Server Ip'),
                  onPressed: () {
                    _prefs.setString('server', _server);
                    _showToastServer(context);
                    if (_server != '') {
                      Future.delayed(const Duration(milliseconds: 2400),
                          () => Navigator.pop(context));
                    }
                  })
            ]));
  }

  _initItems() {
    items = <RouteItem>[
      RouteItem(
          title: 'Conference',
          subtitle: 'Conference',
          push: (BuildContext context) {
            _showStreamIdDialog(context);
          }),
    ];
  }
}

				
			

Create a file named route_item.dart and put the code lines below.

				
					import 'package:flutter/material.dart';
import 'dart:core';

typedef RouteCallback = void Function(BuildContext context);

class RouteItem {
  RouteItem({
    required this.title,
    required this.subtitle,
    required this.push,
  });

  final String title;
  final String subtitle;
  final RouteCallback push;
}
				
			

Create a file named conference.dart and put the lines below.

				
					import 'dart:core';

import 'package:ant_media_flutter/ant_media_flutter.dart';
import 'package:ant_media_flutter/src/utils/conference_widget/playwidget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';

class Conference extends StatefulWidget {
  static String tag = 'call';

  String ip;
  String id;
  String roomId;
  bool userscreen;

  Conference(
      {Key? key,
      required this.ip,
      required this.id,
      required this.roomId,
      required this.userscreen})
      : super(key: key);

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

class _ConferenceState extends State<Conference> {
  final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
  List<Widget> widgets = [];
  bool _inCalling = false;

  _ConferenceState();

  @override
  initState() {
    super.initState();
    initRenderers();
    _connect();
  }

  initRenderers() async {
    await _localRenderer.initialize();
  }

  @override
  deactivate() {
    super.deactivate();
    if (AntMediaFlutter.anthelper != null) AntMediaFlutter.anthelper?.close();
    _localRenderer.dispose();
  }

  void _connect() async {
    AntMediaFlutter.connect(
      //host
      widget.ip,
      //streamID
      widget.id,
      //roomID
      widget.roomId,
      AntMediaType.Conference,
      widget.userscreen,
      false,

      //onStateChange
      (HelperState state) {
        switch (state) {
          case HelperState.CallStateNew:
            setState(() {
              _inCalling = true;
            });
            break;
          case HelperState.CallStateBye:
            setState(() {
              _localRenderer.srcObject = null;
              _inCalling = false;
              Navigator.pop(context);
            });
            break;
          case HelperState.ConnectionOpen:
            break;
          case HelperState.ConnectionClosed:
            break;
          case HelperState.ConnectionError:
            break;
        }
      },

      //onLocalStream
      ((stream) {
        setState(() {
          _localRenderer.srcObject = stream;
        });
      }),

      //onAddRemoteStream
      ((stream) {}),

      // onDataChannel
      (dc) {},

      (dc, message, isReceived) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(
            (isReceived ? "Received:" : "Sent:") + " " + message.text,
            style: const TextStyle(color: Colors.white),
          ),
          backgroundColor: Colors.blue,
        ));
      },

      //onUpdateConferenceUser
      (streams) {
        List<Widget> widgetlist = [];
        for (final stream in streams) {
          SizedBox widget = SizedBox(
            child: PlayWidget(
                ip: this.widget.ip,
                id: stream,
                roomId: this.widget.roomId,
                userscreen: false),
          );
          widgetlist.add(widget);
        }

        setState(() {
          widgets = widgetlist;
        });
      },

      //onRemoveRemoteStream
      ((stream) {
        setState(() {});
      }),
    );
  }

  _hangUp() {
    if (AntMediaFlutter.anthelper != null) {
      AntMediaFlutter.anthelper?.bye();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Conferencing'),
          actions: const <Widget>[],
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
        floatingActionButton: _inCalling
            ? SizedBox(
                width: 200.0,
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      FloatingActionButton(
                        heroTag: "btn2",
                        onPressed: _hangUp,
                        tooltip: 'Hangup',
                        child: const Icon(Icons.call_end),
                        backgroundColor: Colors.pink,
                      ),
                    ]))
            : null,
        body: OrientationBuilder(builder: (context, orientation) {
          Widget local = SizedBox(
            child: RTCVideoView(_localRenderer),
          );

          List<Widget> widgetlist = [local] + widgets;

          return GridView(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2),
            children: widgetlist,
          );
        }));
  }
}

				
			

Your video conferencing app is ready to run

Run flutter packages get to install dependencies.
Run the project with flutter run

WhatsApp Image 2022 10 24 at 11.45.43

You can also get more information about Ant Media Server’s Conference Solution from Create a Conference Solution.

Use Ant Media Server for free for 100 hours every month

You don’t need to pay anything in advance. Just get started and only pay for what you use. Ant Media provides 100 hours of free usage every month and you’ll have Ant Media Server Flutter WebRTC SDK, and everything by signing up with zero upfront cost. So you can start building your own Flutter streaming app now.

You can get started now.

Categories: Tutorial

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published.