随着 2020 年新冠疫情的爆发,远程沟通变得尤为重要,成为人们日常生活中的重要组成部分。如果我们在自己的应用上添加视频通话功能,就可以延长用户留存时间,在用户之间建立联系,从而增加应用产出的价值。那么,怎么添加视频通话功能呢?这就需要用到声网啦:通过 pub.dev 里的插件安装声网 Flutter SDK,用户就可以通过语音、视频或其他媒介进行沟通交流。
本文教大家用通话中数据创建一个简单的视频通话应用。声网功能广泛,但本文从最基础的讲起,方便大家今后自行搭建功能。
项目设置
首先,创建一个账户:
本文搭建的项目只需要用到主文件。然后,运行所有 Flutter 项目都需要运行的代码:
flutter create agora_project
项目搭建完成后,把需要的安装包添加进 pubspec.yaml 文件中。
- agora_rtc_engine:Flutter SDK 的封装器
- permission_handler:查看应用是否已获得摄像头和麦克风的使用权限,也可用于获取设备的其他权限
添加了以上安装包后,你的依赖如下:
dependencies:
flutter:
sdk: flutter
agora_rtc_engine:
permission_handler:
把声网输入到项目中后,你可以把需要用到的所有输入放进项目中。我们要使用 Flutter 提供给用户界面的材料库,还要使用 Dart 的异步库,因为我们要用的很多方法都依赖 Futures(我们将调用声网 Agora SDK,然后等待响应)。其他的输入都来自声网和权限处理器。
声网会使用下面的三个输入:
- RTC Engine:包含大部分实时沟通功能
- RTC Local View:包含当前用户的摄像头视图
- RTC Remote View:包含加入摄像头的另一个用户的视图
最上层的文件如下:
import 'dart:async';
import 'package:flutter/material.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;
import 'package:permission_handler/permission_handler.dart';
最后一步是获取声网的 appId 和令牌,以确保安全使用声网。本项目用的是临时令牌。首先,创建一个声网项目,在 edit 中找到 appId,生成一个临时令牌。然后,在应用中创建两个全局变量,分别命名为 appId
和 token
,并把它们设置为从声网读取到的值。
注意:本项目是为了给大家提供参考,处于开发环境,并非生产环境项目。我们建议所有在生产环境中运行的 RTE 应用都使用令牌验证。
搭建应用
现在,所有的前期设置已完成,我们开始编写代码。如上所述,为了尽可能地简化程序,我们只使用一个文件。首先,用 Material App
中的 Scaffold
创建一个简单的包含状态的微件布局,如下:
void main() => runApp(HomePage());
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Agora Demo"),
),
),
);
}
}
initState 是本应用的一个关键部分,我们要在 initState 中查看权限,并在 initState 中初始化声网Agora 视频通话需要的所有元素。以下情况会在我们初始化应用程序时发生,也会在使用声网 SDK 时随时发生:
- 请求获得摄像头和麦克风权限(如果还没获得权限);
- 创建通信客户端;
- 设置监听器,监听客户端的所有事件变化;
- 为当前设备打开视频;
- 加入频道;
首先,处理权限问题。获得麦克风和摄像头权限之后,才能拨打视频电话。
await [Permission.microphone, Permission.camera].request();
我们主要通过声网通信引擎来处理应用的所有逻辑。要使用声网通信引擎,首先要创建引擎示例,然后用在声网控制台创建的 App ID 来初始化引擎示例。我们把 App ID 设置为 appId
变量。
RtcEngine engine = await RtcEngine.create(appId);
接下来就是应用的主要逻辑:事件处理器负责处理引擎发生的所有事件。我们在引擎上调用setEventHandler
方法,对具体事件进行定义。声网支持多种事件,但我们这个简单应用只使用 4 个事件:
- joinChannelSuccess:当前用户成功加入频道时,触发此事件
- userJoined:当远端用户加入当前用户所在的频道时,触发此事件
- userOffline:当远端用户离开当前用户所在的频道时,触发此事件
- rtcStats:每两秒触发一次并返回通话数据
我们会给上述的 4 个方法设置一些本地变量:第一、设置一个布尔值,该布尔值能告知我们当前用户是否成功加入(_localUserJoined
);第二、把加入频道的远端用户的用户 ID 设置为一个整数,当用户离开频道时,我们会把这个 ID 设置为 null
;第三、每两秒更新一次本地数据变量。
engine.setEventHandler(RtcEngineEventHandler(
joinChannelSuccess: (String channel, int uid, int elapsed) {
print('$uid successfully joined channel: $channel ');
setState(() {
_localUserJoined = true;
});
},
userJoined: (int uid, int elapsed) {
print('remote user $uid joined channel');
setState(() {
_remoteUid = uid;
});
},
userOffline: (int uid, UserOfflineReason reason) {
print('remote user $uid left channel');
setState(() {
_remoteUid = null;
});
},
rtcStats: (stats) {
//updates every two seconds
if (_showStats) {
_stats = stats;
setState(() {});
}
},
));
初始化的最后一步是在引擎中开启视频,让当前用户加入 firstchannel
频道。我们需要传递之前创建的令牌,并把该令牌存储在 token
变量中,从而确保当前用户成功加入频道。
await engine.enableVideo();
await engine.joinChannel(token, 'firstchannel', null, 0);
我们不能在 initState
中调用所有函数, 因为 initState
不能异步,它必须调用另一个函数,因此我们需要创建另一个函数,我们把这个函数命名为 initForAgora,在 initForAgora
内部添加上述所有代码。搭建方法之外的部分如下:
bool _localUserJoined = false;
bool _showStats = false;
int _remoteUid;
RtcStats _stats = RtcStats();
@override
void initState() {
super.initState();
initForAgora();
}
Future<void> initForAgora() async {
// retrieve permissions
await [Permission.microphone, Permission.camera].request();
// create the engine for communicating with agora
RtcEngine engine = await RtcEngine.create(appId);
// set up event handling for the engine
engine.setEventHandler(RtcEngineEventHandler(
joinChannelSuccess: (String channel, int uid, int elapsed) {
print('$uid successfully joined channel: $channel ');
setState(() {
_localUserJoined = true;
});
},
userJoined: (int uid, int elapsed) {
print('remote user $uid joined channel');
setState(() {
_remoteUid = uid;
});
},
userOffline: (int uid, UserOfflineReason reason) {
print('remote user $uid left channel');
setState(() {
_remoteUid = null;
});
},
rtcStats: (stats) {
//updates every two seconds
if (_showStats) {
_stats = stats;
setState(() {});
}
},
));
// enable video
await engine.enableVideo();
await engine.joinChannel(token, 'firstchannel', null, 0);
}
现在我们已完成了应用的所有逻辑。接下来就比较有趣了,我们要为用户展示所有视频和数据,查看这些视频和数据能否正常运行。我们要尽量保持用户界面简约,在 Scaffold
微件的内部展示一个简单的 Stack
,Stack
的底层是另一个用户的实时摄像头的大视图,左上角是当前用户的摄像头,这与其他视频通话应用程序的布局相似。为显示当前用户的视频,我们会使用 RtcLocalView.SurfaceView()
,这是从设置的输入中获取的,另外,从 RtcRemoteView.SurfaceView(uid: _remoteUid)
中获取远程用户的视频,并向 RtcRemoteView.SurfaceView(uid: _remoteUid)
传入特定用户摄像头的 uid。
为显示通话数据,我们将使用 Scaffold
的 floatingActionButton
参数来显示文本为“Show Stats”的按钮或呼叫的实际数据,具体取决于该按钮是否被点击过。统计视图非常简单。如果数据为空,我们会显示一个加载指数;如果有数据,该视图就会显示一列可用的统计信息和一个按钮,该按钮可以关闭该视图并返回到“Show Stats”按钮。
完整的搭建函数如下:
// Create UI with local view and remote view
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter example app'),
),
body: Stack(
children: [
Center(
child: _renderRemoteVideo(),
),
Align(
alignment: Alignment.topLeft,
child: Container(
width: 100,
height: 100,
child: Center(
child: _renderLocalPreview(),
),
),
),
],
),
floatingActionButton: _showStats
? _statsView()
: ElevatedButton(
onPressed: () {
setState(() {
_showStats = true;
});
},
child: Text("Show Stats"),
),
),
);
}
Widget _statsView() {
return Container(
padding: EdgeInsets.all(20),
color: Colors.white,
child: _stats.cpuAppUsage == null
? CircularProgressIndicator()
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("CPU Usage: " + _stats.cpuAppUsage.toString()),
Text("Duration (seconds): " + _stats.totalDuration.toString()),
Text("People on call: " + _stats.users.toString()),
ElevatedButton(
onPressed: () {
_showStats = false;
_stats.cpuAppUsage = null;
setState(() {});
},
child: Text("Close"),
)
],
),
);
}
// current user video
Widget _renderLocalPreview() {
if (_localUserJoined) {
return RtcLocalView.SurfaceView();
} else {
return Text(
'Please join channel first',
textAlign: TextAlign.center,
);
}
}
// remote user video
Widget _renderRemoteVideo() {
if (_remoteUid != null) {
return RtcRemoteView.SurfaceView(uid: _remoteUid);
} else {
return Text(
'Please wait remote user join',
textAlign: TextAlign.center,
);
}
}
完成啦!我们用了大约 100 行 Flutter 代码搭建了一个完整的视频通话应用!
优化应用
我们搭建的应用非常简单,所以很好理解。大家可以用声网进一步优化这个应用,优化步骤非常简单,可以参考下列优化建议:
- 翻转本地摄像头和远端摄像头
- 允许用户输入想加入的频道
- 当发生断联时,屏幕上会显示断联
- 离开通话
- 麦克风静音
- 把说话声音最大的用户的摄像头视图设置的更大一些
- 在应用中添加更多数据
总结
有了声网 Flutter 插件,在 Flutter 应用中添加视频通话功能就变得非常简单。大家可以在本项目的基础上搭建一个功能齐全的视频通话应用,也可以在现有的应用中添加视频通话功能。另外,声网Agora 有很多教大家使用 SDK 的文档和专栏文章,大家可以去看一看哦~~
代码链接: GitHub - tadaspetra/agora-video-call
原文作者:Tadas Petra
原文链接:Building a Flutter Video Call App with in-Call Statistics