Today, we will work together to develop a Flutter application that includes RTE (Real-time interaction) scenarios.

Project introduction

Developing your own apps with real-time interactions can be tedious, with server maintenance, load balancing, and low latency.

So how can real-time interaction be added to Flutter in a short time? You can do this with the Sound Net Agora SDK. In this tutorial I will walk you through how to subscribe to multiple channels using the Agora Flutter SDK. (What is multi-channel like? We’ll give you some examples later.)

The development environment

  • Visit agora. IO to sign up for an Agora developer account.
  • Download the Flutter SDK: docs. Agora. IO/cn/All/down…
  • VS Code or Android Studio has been installed
  • Basic understanding of Flutter development

Why join multiple channels?

Before we get into development, let’s take a look at why people, or live interactive scenarios, need to subscribe to multiple channels.

The main reason for joining multiple channels is to be able to track the real-time interactions of multiple groups at the same time, or to interact with all groups at the same time. Various usage scenarios include online group discussion room, multi-meeting scenario, waiting room, activity meeting, etc.

Project Settings

Let’s start by creating a Flutter project. Open your terminal, find your development folder, and type the following.

flutter create agora_multi_channel_demo
Copy the code

Go to Pubspec.yaml and add the following dependencies to the file.

"> < span style =" font-size: 14px; line-height: 20pxCopy the code

Pay attention to indentation when adding packages, otherwise errors may occur.

In your project folder, run the following command to install all dependencies:

flutter pub get
Copy the code

Once we have all the dependencies, we can create the file structure. Find the lib folder and create a file directory structure like this:

Creating a Login page

The login page simply reads the two channels the user wants to join. For this tutorial, we’ll keep only two channels, but you can add more if you want:

import 'package:agora_multichannel_video/pages/lobby_page.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final rteChannelNameController = TextEditingController(); final rtcChannelNameController = TextEditingController(); bool _validateError = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text('Agora Multi-Channel Demo'), elevation: 0, ), body: SafeArea( child: SingleChildScrollView( clipBehavior: Clip.antiAliasWithSaveLayer, physics: BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ SizedBox( height: Mediaquery.of (context).size. Height * 0.12,), Center(child: Image(Image: NetworkImage( 'https://www.agora.io/en/wp-content/uploads/2019/06/agoralightblue-1.png'), height: Mediaquery.of (context).size. Height * 0.17,),), SizedBox(height: Of (context).size. Height * 0.1,), Container(width: mediaQuery.of (context).size. Width * 0.8, child: TextFormField( controller: rteChannelNameController, decoration: InputDecoration( labelText: 'Broadcast channel Name', labelStyle: TextStyle(color: Colors.black54), errorText: _validateError ? 'Channel name is mandatory' : null, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue, width: 2), borderRadius: BorderRadius.circular(20), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.black, width: 2), borderRadius: BorderRadius.circular(20), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue, width: 2), borderRadius: BorderRadius.circular(20), ), ), ), ), SizedBox( height: Of (context).size. Height * 0.03,), Container(width: mediaQuery.of (context).size. Width * 0.8, child: TextFormField( controller: rtcChannelNameController, decoration: InputDecoration( labelText: 'RTC channel Name', labelStyle: TextStyle(color: Colors.black54), errorText: _validateError ? 'RTC Channel name is mandatory' : null, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue, width: 2), borderRadius: BorderRadius.circular(20), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.black, width: 2), borderRadius: BorderRadius.circular(20), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue, width: 2), borderRadius: BorderRadius. Circular (20),),),),), SizedBox(height: mediaQuery.of (context).size. Height * 0.05), Container(width: Of (context).size. Width * 0.35, Child: MaterialButton(onPressed: onJoin, color: Colors. BlueAccent, child: Symmetric (horizontal: mediaQuery.of (context).size. Width * 0.01, vertical: symmetric(horizontal: MediaQuery.of(context).size. MediaQuery. Of (context). Size. Height * 0.02), the child: Row (mainAxisAlignment: mainAxisAlignment spaceEvenly, children: <Widget>[ Text( 'Join', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), Icon( Icons.arrow_forward, color: Colors.white, ), ], ), ), ), ) ], ), ), ), ); } Future<void> onJoin() async { setState(() { rteChannelNameController.text.isEmpty && rtcChannelNameController.text.isEmpty ? _validateError = true : _validateError = false; }); await _handleCameraAndMic(Permission.camera); await _handleCameraAndMic(Permission.microphone); Navigator.push( context, MaterialPageRoute( builder: (context) => LobbyPage( rtcChannelName: rtcChannelNameController.text, rteChannelName: rteChannelNameController.text, ), ), ); } Future<void> _handleCameraAndMic(Permission permission) async { final status = await permission.request(); print(status); }}Copy the code

When the channel name is successfully submitted, PermissionHandler() is triggered, which is a class from the external package (permission_handler) that we will use to obtain permissions for the user’s camera and microphone during the call.

For now, before we start developing our lobby that can connect multiple channels, keep the App ID separately in utils.dart under utils.dart.

const appID = '<---Enter your App ID here--->';
Copy the code

Create the hall

If you read about multi-person calls or interactive live streams, you’ll see that most of the code we’ll write here is similar. The main difference between the two cases is that previously we relied on a channel to connect to a group. But now one can join more than one channel at a time.

In a single channel video call, we saw how to create an instance of the RtcEngine class and join a channel. Here we start with the same process, as follows:

_engine = await RtcEngine.create(appID);
await _engine.enableVideo();
await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
_addAgoraEventHandlers();
await _engine.joinChannel(null, widget.rteChannelName, null, 0);
Copy the code

Note: This item is intended as a reference in a development environment and is not recommended for production. You are advised to use Token authentication for all RTE apps running in the production environment. For more information about token-based authentication in the Agora platform, please refer to the official audio documentation:docs.agora.io/cn/.

We see that after creating an RtcEngine instance, we need to set the Channel Profile to Live Streaming and add the required channels based on user input.

The _addAgoraEventHandlers() function handles all the major callbacks we need in this project. In the example, I just want to create a list of users in the RTE channel that has their UID.

void _addAgoraEventHandlers() { _engine.setEventHandler(RtcEngineEventHandler( error: (code) { setState(() { final info = 'onError: $code'; _infoStrings.add(info); }); }, joinChannelSuccess: (channel, uid, elapsed) { setState(() { final info = 'onJoinChannel: $channel, uid: $uid'; _infoStrings.add(info); }); }, leaveChannel: (stats) { setState(() { _infoStrings.add('onLeaveChannel'); _users.clear(); }); }, userJoined: (uid, elapsed) { setState(() { final info = 'userJoined: $uid'; _infoStrings.add(info); _users.add(uid); }); }, userOffline: (uid, reason) { setState(() { final info = 'userOffline: $uid , reason: $reason'; _infoStrings.add(info); _users.remove(uid); }); })); }Copy the code

The list of UIds is dynamically maintained because it is updated every time a user joins or leaves a channel.

This sets up our main channel or lobby, where we can display live streamers, now subscribes to other channels require an instance of RtcChannel, only then can you join the second channel.

_channel = await RtcChannel.create(widget.rtcChannelName);
_addRtcChannelEventHandlers();
await _engine.setClientRole(ClientRole.Broadcaster);
await _channel.joinChannel(null, null, 0, ChannelMediaOptions(true, true));
await _channel.publish();
Copy the code

RtcChannel is initialized with the channel name, so we handle this problem with other input from the user. Once it is initialized, we call the join channel function of the ChannelMediaOptions() class, which looks for two parameters: autoSubscribeAudio and autoSubscribeVideo. Since it expects a Boolean value, you can pass true or false as you wish.

For RtcChannel, we see a similar event handler, but we will create another list of users for the users in that particular channel.

void _addRtcChannelEventHandlers() { _channel.setEventHandler(RtcChannelEventHandler( error: (code) { setState(() { _infoStrings.add('Rtc Channel onError: $code'); }); }, joinChannelSuccess: (channel, uid, elapsed) { setState(() { final info = 'Rtc Channel onJoinChannel: $channel, uid: $uid'; _infoStrings.add(info); }); }, leaveChannel: (stats) { setState(() { _infoStrings.add('Rtc Channel onLeaveChannel'); _users2.clear(); }); }, userJoined: (uid, elapsed) { setState(() { final info = 'Rtc Channel userJoined: $uid'; _infoStrings.add(info); _users2.add(uid); }); }, userOffline: (uid, reason) { setState(() { final info = 'Rtc Channel userOffline: $uid , reason: $reason'; _infoStrings.add(info); _users2.remove(uid); }); })); }Copy the code

The _users2 list contains the ids of all the people in the channel created using the RtcChannel class.

With this, you can add multiple channels to your application. Next, let’s look at how we create widgets so that these videos can be displayed on our screens.

Let’s start by adding a view for RtcEngine. In this example, I’ll use a grid view that takes up the most space on the screen.

List<Widget> _getRenderViews() {
    final List<StatefulWidget> list = [];
    list.add(RtcLocalView.SurfaceView());
    return list;
  }

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

  Widget _expandedVideoRow(List<Widget> views) {
    final wrappedViews = views.map<Widget>(_videoView).toList();
    return Expanded(
      child: Row(
        children: wrappedViews,
      ),
    );
  }

  Widget _viewRows() {
    final views = _getRenderViews();
    switch (views.length) {
      case 1:
        return Container(
            child: Column(
          children: <Widget>[_videoView(views[0])],
        ));
      case 2:
        return Container(
            child: Column(
          children: <Widget>[
            _expandedVideoRow([views[0]]),
            _expandedVideoRow([views[1]])
          ],
        ));
      case 3:
        return Container(
            child: Column(
          children: <Widget>[
            _expandedVideoRow(views.sublist(0, 2)),
            _expandedVideoRow(views.sublist(2, 3))
          ],
        ));
      case 4:
        return Container(
            child: Column(
          children: <Widget>[
            _expandedVideoRow(views.sublist(0, 2)),
            _expandedVideoRow(views.sublist(2, 4))
          ],
        ));
      default:
    }
    return Container();
  }
Copy the code

For RtcChannel, I’ll use a scrollable ListView at the bottom of the screen. In this way, users can scroll through the list to see all the users that appear in the channel.

List<Widget> _getRenderRtcChannelViews() { final List<StatefulWidget> list = []; _users2.forEach( (int uid) => list.add( RtcRemoteView.SurfaceView( uid: uid, channelId: widget.rtcChannelName, renderMode: VideoRenderMode.FILL, ), ), ); return list; } Widget _viewRtcRows() { final views = _getRenderRtcChannelViews(); if (views.length > 0) { print("NUMBER OF VIEWS : ${views.length}"); return ListView.builder( scrollDirection: Axis.horizontal, itemCount: views.length, itemBuilder: (BuildContext context, int index) { return Align( alignment: Alignment.bottomCenter, child: Container( height: 200, width: mediaQuery.of (context).size. Width * 0.25, child: _videoView(views[index])),); }); } else { return Align( alignment: Alignment.bottomCenter, child: Container(), ); }}Copy the code

In the call, the style of your application or the way you align the user’s video is entirely up to you. The key elements or widgets to look for are _getRenderViews() and _getRenderRtcChannelViews(), which return a list of user videos. Using this list, you can locate your users and their videos as you choose, similar to _viewRows() and _viewRtcRows() widgets.

Using these widgets, we can add them to our scaffolds. Here, I’ll use a stack to place _viewRows() on _viewRtcRows.

Widg et build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Lobby'),
      ),
      body: Stack(
        children: <Widget>[
          _viewRows(),
          _viewRtcRows(),
          _panel()
        ],
      ),
    );
  }
Copy the code

I’ve added another widget to our stack called _panel, which we use to display all the events happening on our channel.

Widget _panel() { return Container( padding: const EdgeInsets.symmetric(vertical: 48), alignment: Alignment. TopLeft, Child: FractionallySizedBox(heightFactor: 0.5, Child: Container(padding: const EdgeInsets.symmetric(vertical: 48), child: ListView.builder( reverse: true, itemCount: _infoStrings.length, itemBuilder: (BuildContext context, int index) { if (_infoStrings.isEmpty) { return null; } return Padding( padding: const EdgeInsets.symmetric( vertical: 3, horizontal: 10, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Container( padding: const EdgeInsets.symmetric( vertical: 2, horizontal: 5, ), decoration: BoxDecoration( color: Colors.yellowAccent, borderRadius: BorderRadius.circular(5), ), child: Text( _infoStrings[index], style: TextStyle(color: Colors.blueGrey), ), ), ) ], ), ); },),),),); }Copy the code

In this way, users can add two channels and view them simultaneously. But let’s consider an example where you need to add more than two channels to interact in real time. In this case, you can simply create more instances of the RtcChannel class with a unique channel name. Using the same instance, you can join multiple channels.

Finally, you need to create a Dispose () method to clean up the list of users for both channels and call the leaveChannel() method for all the channels we subscribe to.

@override
   void dispose() {
    // clear users
    _users.clear();
    _users2.clear();
    // leave channel 
    _engine.leaveChannel();
    _engine.destroy();
    _channel.unpublish();
    _channel .leaveChannel();
    _channel.destroy();
    super.dispose();
  }
Copy the code

test

Once the app is developed, it allows you to join multiple channels using the Soundnet Agora SDK, and you can run the app and test it on the device. Navigate to the project directory in your terminal and run this command.

flutter run
Copy the code

conclusion

You have implemented your own live App with the Agora Flutter SDK, which allows you to add multiple channels simultaneously.

Get this article Demo: github.com/Meherdeep/a…

For more tutorials, demos, and technical help, click here”Read the original text”Visit the Soundnet developer community.