大数跨境
0
0

Agora 教程 | 利用Agora Flutter SDK开发多人视频通话APP

Agora 教程 | 利用Agora Flutter SDK开发多人视频通话APP RTE开发者社区
2021-05-13
0
导读:在这篇文章中,我们将看到如何使用Agora Flutter SDK实现自己的Flutter多人视频通话APP
在这篇文章中,我们将看到如何使用Agora Flutter SDK实现自己的Flutter多人视频通话APP。
开发要求
如果你是Flutter的新手,那么从Flutter官方网站(如下链接)安装Flutter SDK:
https://flutter.dev/docs/get-started/install
  • 下载 Agora Flutter SDK:https://docs.agora.io/cn/All/downloads?platform=Flutter

  • VS Code或其他你选择的IDE

  • 访问网址 Agora.io,注册Agora开发者账户

项目设置
1. 我们先创建一个Flutter项目。打开你的终端,找你的开发文件夹,然后输入以下内容。

flutter create agora_group_calling

2. 找到pubspec.yaml文件。在该文件中,添加以下依赖项。

dependencies:  flutter:    sdk: flutter  cupertino_icons: ^1.0.0  permission_handler: ^5.1.0+2  agora_rtc_engine: ^3.2.1


在添加包的时候要注意缩进,因为如果缩进不对,可能会出错。

3. 在项目文件夹中,运行以下命令来安装所有的依赖项。


flutter pub get

4. 一旦我们有了所有的依赖项,我们就可以创建文件结构了。导航到lib文件夹,创建一个像这样的文件结构:

创建群组视频通话界面
首先,找到main.dart。用下面的代码替换模板代码。

import 'package:flutter/material.dart';import 'Screens/homepage.dart';
void main() { runApp(MyApp());}
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); }}

这段代码只是初始化你的Flutter应用程序,并调用我们在HomePage.dart中定义的HomePage.dart。

创建我们的主页

继续创建我们的主页,我们将要求用户输入一个频道名。一个频道名是一个唯一的字符串,它将把具有相同频道名的人放在一个群组里调用。


import 'package:flutter/material.dart';import 'package:permission_handler/permission_handler.dart';import 'dart:async';
import 'CallPage.dart';
class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState();}
class _MyHomePageState extends State<MyHomePage> { final myController = TextEditingController(); bool _validateError = false;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text('Agora Group Video Calling'), elevation: 0, ), body: SafeArea( child: Center( child: SingleChildScrollView( clipBehavior: Clip.antiAliasWithSaveLayer, physics: BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Image( image: NetworkImage( 'https://www.agora.io/en/wp-content/uploads/2019/07/agora-symbol-vertical.png'), height: MediaQuery.of(context).size.height * 0.17, ), Padding(padding: EdgeInsets.only(top: 20)), Text( 'Agora Group Video Call Demo', style: TextStyle( color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), ), Padding(padding: EdgeInsets.symmetric(vertical: 20)), Container( width: MediaQuery.of(context).size.width * 0.8, child: TextFormField( controller: myController, decoration: InputDecoration( labelText: 'Channel Name', labelStyle: TextStyle(color: Colors.blue), hintText: 'test', hintStyle: TextStyle(color: Colors.black45), errorText: _validateError ? 'Channel name is mandatory' : null, border: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), borderRadius: BorderRadius.circular(20), ), ), ), ), Padding(padding: EdgeInsets.symmetric(vertical: 30)), Container( width: MediaQuery.of(context).size.width * 0.25, child: MaterialButton( onPressed: onJoin, height: 40, color: Colors.blueAccent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Join', style: TextStyle(color: Colors.white), ), Icon( Icons.arrow_forward, color: Colors.white, ), ], ), ), ) ], ), ), ), ), );


这样操作将创建一个类似于左图的用户界面,其中有一个频道名的输入栏和一个加入按钮。加入按钮会调用函数onJoin,它首先获取用户在通话过程中访问其摄像头和麦克风的权限。一旦用户授予这些权限,我们就进入下一个页面,CallPage.dart

为了要求用户访问摄像头和麦克风,我们使用了一个名为permission_handler.的包。这里我声明了一个名为_handleCameraAndMic(),的函数,我将在onJoin()函数中引用它 。 


Future<void> _handleCameraAndMic(Permission permission) async {    final status = await permission.request();    print(status); }

现在,在我们的onJoin()函数中,我们为上面的函数创建引用,然后将用户提交的频道名称传递给下一个页面,CallPage.dart。

Future<void> onJoin() async {    setState(() {      myController.text.isEmpty          ? _validateError = true          : _validateError = false;    });
await _handleCameraAndMic(Permission.camera); await _handleCameraAndMic(Permission.microphone);
Navigator.push( context, MaterialPageRoute( builder: (context) => CallPage(channelName: myController.text), )); }


创建我们的呼叫页面

在我们开始使用CallPage.dart之前,让我们使用从Agora开发者账户中获得的App ID。(按照声网开发者注册使用指南的说明学习如何生成一个App ID。)导航到utils文件夹中的AppID.dart并创建一个名为appID的变量。


var appID = '<--- Enter your app id here --->' 

在这之后,我们移动到我们的CallPage.dart,并且开始导入所有的文件。

import 'package:flutter/material.dart';import '../utils/AppID.dart';import 'package:agora_rtc_engine/rtc_engine.dart';import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;

在这里,我创建了一个名为CallPage的状态组件,这样它的构造函数就可以读取用户提交的频道名称。

class CallPage extends StatefulWidget {  final String channelName;  const CallPage({Key key, this.channelName}) : super(key: key);
@override _CallPageState createState() => _CallPageState();}


然后在CallPageState中声明一些变量,我们将在制作这个页面时使用这些变量。

  • _users是一个列表, 它包含了频道中所有用户的uid 。

  • _infoStrings包含了所有调用过程中发生的所有事件的日志。

  • muted 是一个布尔状态变量,用于静音或者取消静音。

  • _engine 是RtcEngine类的一个对象。

  • 在dispose方法中,清除_users列表并销毁RtcEngine。

  • 在initState()方法中,调用将在接下来的步骤中声明的initialize()函数。


 class _CallPageState extends State<CallPage> {  static final _users = <int>[];  final _infoStrings = <String>[];  bool muted = false;  RtcEngine _engine;
@override void dispose() { // clear users _users.clear(); // destroy sdk _engine.leaveChannel(); _engine.destroy(); super.dispose(); }
@override void initState() { super.initState(); // initialize agora sdk initialize(); }}

我们将创建initialize()函数,使其成为所有主要函数的共同调用。initialize()函数的主要用途是初始化Agora SDK。在initize函数中,创建_initAgoraRtcEngine() 和_addAgoraEventHandlers()函数的引用。

Future<void> initi alize() async {    if (appID.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);  }

_initAgoraRtcEngine()是作为Agora SDK的实例使用的。使用你从Agora控制台得到的App ID来初始化它。另外使用enableVideo()函数来启用视频模块。这个函数可以在加入频道之前调用,也可以在调用过程中调用。如果你在加入频道之前调用它,那么调用默认是以视频模式启动的。否则,它会以音频模式启动应用程序,如果需要的话,后面可以切换到视频模式。

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


  • _addAgoraEventHandlers()是一个处理所有主要回调函数的函数。所以我们从setEventHandler()开始,它监听引擎事件并接收相应RtcEngine的统计数据。

  • 一些重要的回调包括

  • joinChannelSuccess()在本地用户加入指定频道时被触发。它返回频道名、用户的id和本地用户加入通道的时间(ms)。

  • leaveChannel()与之相反,因为它是在用户离开频道时触发的。每当用户离开频道时,它就会返回调用的统计信息。这些统计包括延迟、CPU使用量、持续时间等。

  • userJoined()是一个当远程用户加入一个特定频道时被触发的方法。一个成功的回调会返回远程用户的id和经过的时间。

  • userOffline() 与之相反,因为它发生在用户离开频道的时候。一个成功的回调会返回uid和离线的原因,包括退出、中断等。

  • firstRemoteVideoFrame()是一个当远程视频的第一个视频帧被渲染时被调用的方法。这可以帮助你返回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);        });      },      firstRemoteVideoFrame: (uid, width, height, elapsed) {        setState(() {          final info = 'firstRemoteVideoFrame: $uid';          _infoStrings.add(info);        });      },    ));

为了结束 initialize()函数,我们来添加 joinChannel()函数。频道是一个可以让人们进行同一个视频通话的房间。joinChannel()方法可以用这样的方式调用:

await _engine.joinChannel(null'channel-name'null0);


它需要四个参数才能成功运行

  • Token:它是一个可选的字段,在测试时可以为空,但在切换到生产环境时应该由Token服务器生成。

  • 频道名称:它是一个字符串,让用户进入一个视频通话。

  • 可选信息:这是一个可选字段,你可以通过它传递有关频道的其他信息。

  • uid:它是每个加入频道的用户的唯一ID。如果你在其中传递0或空值,那么Agora会自动为每个用户分配uid。

以上总结了制作这个视频调用应用程序所需的所有功能和方法。现在我们可以开始制作组件,它将成为我们应用程序的完整用户界面。

这里我声明了两个组件(_viewRows()和_toolbar()),这两个组件负责显示最多四个用户,并在底部添加了断开、静音和切换摄像头的按钮。


@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(),            _toolbar(),          ],        ),      ),    );  }


我们从_viewRows()开始。因此需要知道用户和他们的uid来显示他们的视频。我们需要一个具有本地和远程用户的uid的通用列表。为了实现这一点,我们创建一个名为_getRendererViews()的组件,其中我们使用RtcLocalView和RtcRemoteView。

然后,我们只需使用名为_videoView()的组件来扩展视图 ,并使用_expandedVideoRow()组件将它们放在一行中。


/// Helper fun ction to get list of native views  List<Widget> _getRenderViews() {    final List<StatefulWidget> list = [];    list.add(RtcLocalView.SurfaceView());    _users.forEach((int uid) => list.add(RtcRemoteView.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, ), ); }

一旦我们有了正确的视图结构,我们可以使用一个switch case进行硬编码设计,它在视图堆叠的地方创建列。

 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();  }Widget _toolbar() {    return Container(      alignment: Alignment.bottomCenter,      padding: const EdgeInsets.symmetric(vertical: 48),      child: Row(        mainAxisAlignment: MainAxisAlignment.center,        children: <Widget>[          RawMaterialButton(            onPressed: _onToggleMute,            child: Icon(              muted ? Icons.mic_off : Icons.mic,              color: muted ? Colors.white : Colors.blueAccent,              size: 20.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: muted ? Colors.blueAccent : Colors.white,            padding: const EdgeInsets.all(12.0),          ),          RawMaterialButton(            onPressed: () => _onCallEnd(context),            child: Icon(              Icons.call_end,              color: Colors.white,              size: 35.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: Colors.redAccent,            padding: const EdgeInsets.all(15.0),          ),          RawMaterialButton(            onPressed: _onSwitchCamera,            child: Icon(              Icons.switch_camera,              color: Colors.blueAccent,              size: 20.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: Colors.white,            padding: const EdgeInsets.all(12.0),          )        ],      ),    );  }

到这里,我们实现了一个完整的Flutter多人视频通话APP。现在,为了添加断开通话、静音和切换摄像头等功能,我们需要创建一个名为_toolbar()的基本组件,它有三个部分:

Widget _toolbar() {    return Container(      alignment: Alignment.bottomCenter,      padding: const EdgeInsets.symmetric(vertical: 48),      child: Row(        mainAxisAlignment: MainAxisAlignment.center,        children: <Widget>[          RawMaterialButton(            onPressed: _onToggleMute,            child: Icon(              muted ? Icons.mic_off : Icons.mic,              color: muted ? Colors.white : Colors.blueAccent,              size: 20.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: muted ? Colors.blueAccent : Colors.white,            padding: const EdgeInsets.all(12.0),          ),          RawMaterialButton(            onPressed: () => _onCallEnd(context),            child: Icon(              Icons.call_end,              color: Colors.white,              size: 35.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: Colors.redAccent,            padding: const EdgeInsets.all(15.0),          ),          RawMaterialButton(            onPressed: _onSwitchCamera,            child: Icon(              Icons.switch_camera,              color: Colors.blueAccent,              size: 20.0,            ),            shape: CircleBorder(),            elevation: 2.0,            fillColor: Colors.white,            padding: const EdgeInsets.all(12.0),          )        ],      ),    );  }


这里我们声明了三个函数。

  • _onToggleMute()可以让你的视频流静音或者取消静音。这里,我们使用muteLocalAudioStream() 方法,它接受一个布尔值输入来使视频流静音或取消静音 。


void _onToggleMute() {    setState(() {      muted = !muted;    });    _engine.muteLocalAudioStream(muted);  }

  • _onCallEnd()断开呼叫并将用户带回主页 。



 void _onCallEnd(BuildContext context) {    Navigator.pop(context);  }
  • _onSwitchCamera()可以让你在前摄像头和后摄像头之间切换。在这里,我们使用switchCamera()方法,它可以帮助你实现所需的功能。


结论

现在你已经实现了Flutter多人视频通话APP,使用了Agora Flutter SDK,并实现了一些基本功能,如静音本地视频流、切换摄像头和断开通话。你可以在声网多人通话应用示例代码得到这个应用程序的完整代码:

https://github.com/Meherdeep/agora-group-calling

获取更多教程、Demo、技术帮助,请点击「阅读原文」访问声网开发者社区


【声明】内容源于网络
0
0
RTE开发者社区
RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
内容 1122
粉丝 0
RTE开发者社区 RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
总阅读197
粉丝0
内容1.1k