3

Building a Scalable UI for Your Flutter Application Using Agora

 2 years ago
source link: https://dev.to/meherdeep/building-a-scalable-ui-for-your-flutter-application-using-agora-52pf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Cover image for Building a Scalable UI for Your Flutter Application Using Agora
Meher

Posted on Oct 12

Building a Scalable UI for Your Flutter Application Using Agora

One of the biggest challenges facing any developer is building applications that can scale. With a video-conferencing application using Agora, the main scaling issue is the bandwidth of your local device, especially if there are many incoming video streams. And as the number of participants rises, it becomes increasingly difficult to make sure that your application can keep up.
In this tutorial, you will see how to use Agora to build an application that can scale to up to 17 users by optimizing the bandwidth usage of the incoming video streams.

Pre-requisites

Project Setup

  1. In your project directory, open a terminal window and create a Flutter project by running this command:
    flutter create agora_scalable_ui

  2. Open the newly created project in the editor of your choice.
    Navigate to pubspec.yaml and add the latest version of the Agora Flutter SDK as a dependency.

  3. Add relevant permission in your AndroidManifest.xml file (Android) or Info.plist file (iOS):

  • Android (in your AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET"/> 
<uses-permission android:name="android.permission.RECORD_AUDIO"/> 
<uses-permission android:name="android.permission.CAMERA"/>
Enter fullscreen modeExit fullscreen mode

All done! Let's start building the app.

Build

We will stick with a very simple grid view. Our goal is to scale this application as the user joins. To do this, we will optimize our bandwidth usage to ensure all the streams work concurrently without any difficulty.

Building a Home Page

Our home page is responsible for prompting the user for the name of the channel they want to join and requesting camera and microphone permission.

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

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

class _MyHomePageState extends State<MyHomePage> { TextEditingController _channelController = TextEditingController();

@override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( physics: BouncingScrollPhysics(), child: Container( height: MediaQuery.of(context).size.height, width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( 'Agora Scalable UI', style: TextStyle(fontSize: 30), ), Container( width: MediaQuery.of(context).size.width * 0.7, child: TextFormField( controller: _channelController, decoration: InputDecoration(hintText: 'Channel Name'), ), ), Container( width: MediaQuery.of(context).size.width * 0.5, child: MaterialButton( color: Colors.blue, onPressed: () async { await [Permission.camera, Permission.microphone].request(); Navigator.of(context).push(MaterialPageRoute( builder: (context) => VideoCallPage(channelName: _channelController.text), )); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Submit', style: TextStyle(color: Colors.white), ), Icon( Icons.arrow_right, color: Colors.white, ) ], ), ), ), ], ), ), ), ); } }

To request user permission, we are using a plug-in called permission_handler.

Building the video call page

  1. Adding all the required variables:

static final _users = <int>[]; final _infoStrings = <String>[]; bool muted = false; late RtcEngine _engine;

  1. Initializing the Agora methods:

@override void initState() { super.initState(); // initialize agora sdk initialize(); }

Future<void> initialize() async { if (APP_ID.isEmpty) { setState(() { _infoStrings.add('APP_ID missing, please provide your APP_ID in settings.dart',); _infoStrings.add('Agora Engine is not starting'); }); return; } await _initAgoraRtcEngine(); _addAgoraEventHandlers(); await _engine.joinChannel(null, widget.channelName, null, 0); await _engine.enableDualStreamMode(true); await _engine.setParameters(""" { "che.video.lowBitRateStreamParameter": { "width":160,"height":120,"frameRate":5,"bitRate":45 }} """); }

In the above code snippet, we set up the Agora engine with dual-stream mode enabled. Dual-stream is an Agora feature that allows a client to publish two streams at the same time. One stream is for a higher resolution and bitrate, and the other stream is for a lower resolution and bitrate. With this dual-stream setup, when a remote client subscribes to your stream, they can switch to a lower stream based on their bandwidth requirements.

  1. Initializing the Agora SDK:

Future<void> _initAgoraRtcEngine() async { _engine = await RtcEngine.create(APP_ID); await _engine.enableVideo(); }

The _initAgoraRtcEngine function creates an object of the AgoraRtcEngine, which we use to refer to all the methods provided in the Agora SDK.

  1. Adding Agora event handlers:

void _addAgoraEventHandlers() { _engine.setEventHandler(RtcEngineEventHandler( 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); }); if (_users.length >= 5) { print("Fallback to Low quality video stream"); _engine.setRemoteDefaultVideoStreamType(VideoStreamType.Low); } }, userOffline: (uid, reason) { setState(() { final info = 'userOffline: $uid , reason: $reason'; _infoStrings.add(info); _users.remove(uid); }); if (_users.length <= 3) { print("Go back to High quality video stream"); _engine.setRemoteDefaultVideoStreamType(VideoStreamType.High); } }, )); }

In the userJoined callback, we are doing the following:
Adding the UID of the remote video to a list of UIDs (_users) that we use as the dataset for all the users' UIDs present in the channel
Checking if the number of remote users has grown to five to know whether to switch to using the lower stream in dual-stream mode

In the userOffline callback, we are doing the following:
Removing the UID from the _users list
Switching back to the higher stream in the dual-stream mode if the number of remote users shrinks to three

  1. Creating the grid view for local and remote user video

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Agora Group Video Calling'), ), backgroundColor: Colors.black, body: Center( child: Stack( children: <Widget>[ _viewRows(), ], ), ), ); }

/// Helper function to get list of native views List<Widget> _getRenderViews() { final List<StatefulWidget> list = []; list.add(rtc_local_view.SurfaceView()); _users.forEach( (int uid) => list.add( rtc_remote_view.SurfaceView( uid: uid, ), ), ); return list; }

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

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

/// Video layout wrapper Widget _viewRows() { final views = _getRenderViews(); if (views.length == 1) { return Container( child: Column( children: <Widget>[_videoView(views[0])], ), ); } else if (views.length == 2) { return Container( child: Column( children: <Widget>[ _expandedVideoRow([views[0]]), _expandedVideoRow([views[1]]) ], )); } else if (views.length > 2 && views.length % 2 == 0) { return Container( child: Column( children: [ for (int i = 0; i < views.length; i = i + 2) _expandedVideoRow( views.sublist(i, i + 2), ), ], ), ); } else if (views.length > 2 && views.length % 2 != 0) { return Container( child: Column( children: <Widget>[ for (int i = 0; i < views.length; i = i + 2) i == (views.length - 1) ? _expandedVideoRow(views.sublist(i, i + 1)) : _expandedVideoRow(views.sublist(i, i + 2)), ], ), ); } return Container(); }

Here, we simply pass the UID present in the _users list to generate a list of views. These views are then used to generate a grid that scales automatically as the user joins.

Cleaning Up

Finally, it is time to write the dispose method. We clean up all the resources relevant to Agora here:

@override void dispose() { // clear users _users.clear(); // destroy sdk _engine.leaveChannel(); _engine.destroy(); super.dispose(); }

Testing

To test the application, you will need five or more users such that when they join the channel the video quality optimizes automatically in order to provide the best video calling experience.

To run the app, enter this command in your terminal:

flutter run
Enter fullscreen modeExit fullscreen mode

Conclusion

You now have a video chat application that can scale to up to 17 users by optimizing settings for incoming streams.

You can find a complete application using all of the above code on GitHub

Other Resources

To learn more about the Agora Flutter SDK and other use cases, see the developer guide here.
You can also have a look at the complete documentation for the functions discussed above and many more here.
And I invite you to join the Agora.io Developer Slack Community.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK