利用Agora Flutter SDK开发多人视频通话APP
在这篇文章中,我们将看到如何使用Agora Flutter SDK实现自己的Flutter多人视频通话APP。
先决条件
如果你是Flutter的新手,那么从Flutter官方网站安装Flutter SDK。
项目设置
1.我们先创建一个Flutter项目。打开你的终端,找你的开发文件夹,然后输入以下内容。
flutter create agora_group_calling
-
找到
pubspec.yaml
文件。在该文件中,添加以下依赖项。dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.0 permission_handler: ^5.1.0+2 agora_rtc_engine: ^3.2.1
pubspec.yaml
在添加包的时候要注意缩进,因为如果缩进不对,可能会出错。
- 在项目文件夹中,运行以下命令来安装所有的依赖项:
flutter pub get
4.一旦我们有了所有的依赖项,我们就可以创建文件结构了。导航到lib文件夹,创建一个像这样的文件结构:
Project Structure
创建群组视频通话界面
首先,找到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,
),
],
),
),
)
],
),
),
),
),
);
HomePage.dart
HomePage UI
这样操作将创建一个类似于左图的用户界面,其中有一个频道名的输入栏和一个加入按钮。加入按钮会调用函数 onJoin
,它首先获取用户在通话过程中访问其摄像头和麦克风的权限。一旦用户授予这些权限,我们就进入下一个页面,CallPage.dart。
为了要求用户访问摄像头和麦克风,我们使用了一个名为permission_handler.的包。这里我声明了一个名为 _handleCameraAndMic(),
的函数,我将在 onJoin()
函数中引用它 。
Future<void> _handleCameraAndMic(Permission permission) async {
final status = await permission.request();
print(status);
}
HomePage.dart — _handleCameraAndMic()
现在,在我们的 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;
importing the Agora SDK
在这里,我创建了一个名为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(); } }
Agora CallPageState
我们将创建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();
}
_initAgoraRtcEngine()
_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); }); }, ));
_addAgoraEventHandlers()
为了结束 initialize()函数,我们来添加 joinChannel()
函数。频道是一个可以让人们进行同一个视频通话的房间。joinChannel()方法可以用这样的方式调用:
await _engine.joinChannel(null, 'channel-name', null, 0);
oinChannel()
它需要四个参数才能成功运行
- 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(),
],
),
),
);
}
build
我们从 _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,并实现了一些基本功能,如静音本地视频流、切换摄像头和断开通话。
你可以在声网多人通话应用示例代码得到这个应用程序的完整代码。
其他资源
要了解更多关于Agora Flutter SDK和其他用例的信息,你可以参考这里的开发者指南。
您也可以在这里查看上面讨论的功能和其他许多功能的声网Flutter完整文档。
获取更多文档、Demo、技术帮助
- 获取 SDK 开发文档,可访问声网文档中心。
- 如需参考各类场景 Demo,可访问下载中心获取。
- 如遇开发疑难,可访问论坛发帖提问。
- 了解更多教程、RTE 技术干货与技术活动,可访问声网开发者社区。