今天,我们将会一起开发一个包含 RTE (实时互动)场景的 Flutter 应用。

项目介绍

靠自研开发包含实时互动功能的应用非常繁琐,你要解决维护服务器、负载均衡等难题,同时还要保证稳定的低延迟。

那么,如何才能在较短的时间内,将实时互动功能添加到 Flutter 应用中?你可以通过声网Agora SDK 来进行开发。在本教程中,我将带大家了解如何使用 Agora Flutter SDK 订阅多个频道的过程。(多频道是什么样场景呢?我们稍后举些例子。)

开发环境

  • 网页访问 Agora.io,注册一个Agora开发者账户。

  • 下载 Flutter SDK:https://docs.agora.io/cn/All/downloads

  • 已安装 VS Code 或 Android Studio

  • 对 Flutter 开发的基本了解

为什么要加入多个频道?

在进入正式开发之前,我们先看看为什么有人或者说实时互动场景需要订阅多个频道。

加入多个频道的主要原因是可以同时跟踪多个群组的实时互动活动,或者同时与各个群组互动。各种使用场景包括线上的分组讨论室、多会议场景、等待室、活动会议等。

项目设置

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

flutter create agora_multi_channel_demo

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

dependencies:
flutter:
sdk: flutter


cupertino_icons: ^1.0.0
agora_rtc_engine: ^3.2.1
permission_handler: ^5.1.0+2
 

在添加包的时候要注意这边的缩进,否则可能会出现错误。

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

flutter pub get

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

创建登录页面

登录页面只需读取用户想要加入的两个频道即可。在本教程中,我们只保留两个频道,但如果你想的话也可以加入更多的频道:

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: MediaQuery.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: MediaQuery.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: MediaQuery.of(context).size.width * 0.35,
child: MaterialButton(
onPressed: onJoin,
color: Colors.blueAccent,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.01,
vertical: MediaQuery.of(context).size.height * 0.02),
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);
}
}
 

在成功提交频道名称时,会触发 PermissionHandler(),这是一个来自外部包(permission_handler)的类,我们将使用这个类来获取用户在调用过程中的摄像头和麦克风的权限。

现在,在我们开始开发我们的可以连接多个频道的大厅之前,在 utils.dart 文件夹下的 utils.dart 中单独保留 App ID。

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

创建大厅

如果你了解过多人通话或互动直播,你会发现,我们在这里要写的大部分代码是相似的。这两种情况下的主要区别是,之前我们是依靠一个频道来连接一个群组。但是现在一个人可以同时加入多个频道。

在一个单频道视频通话中,我们看到了如何创建一个 RtcEngine 类的实例并加入一个频道。在这里我们也是以同样的过程开始的,如下:

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

注意:该项目是作为开发环境下的参考,不推荐用于生产环境。建议在生产环境中运行的所有 RTE App 都使用Token鉴权。关于 Agora 平台中基于 Token 的身份验证的更多信息,请参考声网官方文档:https://docs.agora.io/cn/

我们看到,在创建一个RtcEngine实例后,需要将Channel Profile设置为Live Streaming,并根据用户输入加入所需的频道。

_addAgoraEventHandlers() 函数处理了我们在这个项目中需要的所有主要回调。在示例中,我只是想在有他们的 uid 的 RTE 频道中创建一个用户列表。

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);
});
},
));
}

uid 的列表是动态维护的,因为每次用户加入或离开频道时它都会更新。

这就设置了我们的主频道或大厅,在这里可以显示主播直播,现在订阅其他频道需要一个 RtcChannel 的实例,只有这样你才能加入第二个频道。

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

RtcChannel 是用频道名来初始化的,所以我们用用户给的其他输入来处理这个问题。一旦它被初始化,我们调用 ChannelMediaOptions() 类的加入频道函数,这个类寻找两个参数:autoSubscribeAudio 和autoSubscribeVideo。由于它期望的是一个布尔值,你可以根据你的要求传递 ture 或 false。

对于 RtcChannel,我们看到了类似的事件处理程序,不过我们将为该特定频道中的用户创建另一个用户列表。

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);
});
},
));
}
 

_users2 列表中包含了使用 RtcChannel 类创建的频道中所有人的 ID。

有了这个,你就可以在你的应用程序中添加多个频道。接下来,让我们看看我们如何创建 Widget,以便这些视频可以显示在我们的屏幕上。

我们首先添加 RtcEngine 的视图。在这个例子中,我将使用一个占据屏幕最大空间的网格视图。

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();
}

对于 RtcChannel,我将使用一个位于屏幕底部的可滚动的 ListView。这样一来,用户可以通过滚动列表来查看所有出现在频道中的用户。

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

在调用中,你的应用程序的风格或对齐用户视频的方式完全由你决定。需要寻找的关键元素或小组件是 _getRenderViews() 和 _getRenderRtcChannelViews(),它们返回一个用户视频列表。使用这个列表,你可以按照你的选择来定位你的用户和他们的视频,类似于 _viewRows() 和 _viewRtcRows() 小组件。

使用这些小组件,我们可以将它们添加到我们的支架上。在这里,我将使用一个堆栈将_viewRows() 放在 _viewRtcRows 之 上。

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

我已经在我们的堆栈中添加了另一个名为 _panel 的小组件,我们使用这个小组件来显示我们频道上发生的所有事件。

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

这样一来,用户就可以添加两个频道并且同时查看。但是让我们思考一个例子,在这个例子中,你需要加入两个以上的频道实时互动。在这种情况下,你可以用一个独特的频道名称简单地创建更多的 RtcChannel 类的实例。使用同一个实例,你就可以加入多个频道。

最后,你需要创建一个 dispose() 方法,来清除两个频道的用户列表,并为我们订阅的所有频道调用 leaveChannel() 方法。

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

测试

当应用完成开发后,通过它你可以使用声网Agora SDK 加入多个频道,你可以运行应用并在设备上测试。在你的终端中导航到项目目录,并运行这个命令。

flutter run

结论

通过能够同时加入多个频道的声网Agora Flutter SDK,你已经实现了你自己的直播 App。

获取本文 Demo:https://github.com/Meherdeep/agora-flutter-multi-channel

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

如何基于 React Native 快速实现一个视频通话应用的更多相关文章

  1. 基于React Native的58 APP开发实践

    React Native在iOS界早就炒的火热了,随着2015年底Android端推出后,一套代码能运行于双平台上,真正拥有了Hybrid框架的所有优势.再加上Native的优秀性能,让越来越多的公司 ...

  2. 基于React Native的移动平台研发实践分享

    转载:http://blog.csdn.net/haozhenming/article/details/72772787 本文目录: 一.React Native 已经成为了移动前端技术的趋势 二.基 ...

  3. 基于React Native的Material Design风格的组件库 MRN

    基于React Native的Material Design风格的组件库.(为了平台统一体验,目前只打算支持安卓) 官方网站 http://mrn.js.org/ Github https://git ...

  4. 用react native 做的一个推酷client

    用react native 做的一个推酷client 仅供大家參考.仅仅为抛砖引玉.希望大家能以此来了解react.并编写出很多其它的优质的开源库,为程序猿做出贡献. 用的的组件: Navigator ...

  5. 基于React Native的跨三端应用架构实践

    作者|陈子涵 编辑|覃云 “一次编写, 到处运行”(Write once, run anywhere ) 是很多前端团队孜孜以求的目标.实现这个目标,不但能以最快的速度,将应用推广到各个渠道,而且还能 ...

  6. NodeJS笔记(五) 使用React Native 创建第一个 Android APP

    参考:原文地址 几个月前官方推出了快速创建工具包,由于对React Native不熟悉这里直接使用这2个工具包进行创建 1. create-react-native-app(下文简称CRNA): 2. ...

  7. React Native 快速入门之认识Props和State

    眼下React Native(以后简称RN)越来越火,我也要投入到学习当中.对于一个前端来说,还是有些难度.因为本人觉得这是一个App开发的领域,自然是不同.编写本文的时候,RN的版本为0.21.0. ...

  8. 如何在 React Native 中写一个自定义模块

    https://my.oschina.net/jpushtech/blog/983230

  9. H5、React Native、Native应用对比分析

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博!iOS开发者交流QQ群: 446310206 "存在即合理".凡是存在的,都是合乎规律的.任何新 ...

  10. 快速搭建一个基于react的项目

    最近在学习react,快速搭建一个基于react的项目 1.创建一个放项目文件夹,用编辑器打开 2.打开集成终端输入命令: npm install -g create-react-app 3. cre ...

随机推荐

  1. lua 添加的时候去重

    result = {} ids = {1,9,6,7}affs = {3,2,4,5,6}count =0for s in *ids result[s]=sfor p, v in pairs resu ...

  2. 2020-2021第一学期2024"DCDD"小组第十二周讨论

    2020-2021第一学期"DCDD"第十二周讨论 这次不同的是,先来一个密文吧: 53fd95b7c2bd8c1383cdcbf5b04e3880 求解! 小组名称:DCDD 小 ...

  3. 04 ajax执行php并传递参数

    做这个事情之前要导入jQuery js的方式 _this.value1 = "abc"; _this.value2 = 1; $.ajax({ url: 'xxxxxx.php', ...

  4. Vue3 + echarts 统一封装

    1. 新建 echartsLib.js 文件,统一导入需要的组件 import * as echarts from "echarts/core"; import { SVGRend ...

  5. scala的运算符

    1.算数运算符 与java基本一样,只有个别细节不一样 (1).除法的区别:整数/整数 结果为整数(小数部分直接舍掉了):小数/整数 结果为小数: 例如:val result = 10.0 / 3 p ...

  6. react intl 国际化

    方案描述:由于采用单页面,所以按钮切换时会刷新页面 1.安装 react-intl  babel-plugin-react-intl json-loader npm i react-intl babe ...

  7. Jupyter lab 切换kernel

    在使用pytorch的时候需要用到pandas这个包,报错说"no module named pandas", 但是我在终端查找了conda 装了pandas,所以不是安装的问题, ...

  8. spring boot2.3.0集成 thymelaf

    配置pom 如果是2.x的直接配置一个starter即可  <!-- ThymeLeaf 依赖 --><dependency>  <groupId>org.spri ...

  9. awk统计命令

    求和 cat file|awk '{sum+=$1} END {print "Sum = ", sum}' cat file|awk '{sum[$1]+=$2}END{for(c ...

  10. UDP与TCP ---FundeBug

    UDP 面向无连接 首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了.并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作. 具体来说就是: ...