0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何快速构建一个移动跨平台视频通话应用

BYXG_shengwang 来源:xx 2019-02-24 06:01 次阅读

上周,我们发布了Agora Flutter SDK之后,吸引了 Flutter 社区的诸多关注。Google Flutter 与 Dart 的产品负责人 Tim Sneath,在看到 Agora Flutter SDK 后,还特意在社交媒体上发推点赞。

今天我们就来看一下如何使用 Agora Flutter SDK 快速构建一个简单的移动跨平台视频通话应用。

环境准备

在 Flutter 中文网(flutterchina.club)上,关于搭建开放环境的教程已经相对比较完善了,有关 IDE 与环境配置的过程本文不再赘述,若 Flutter 安装有问题,可以执行 flutter doctor 做配置检查。

本文使用 MacOS 下的 VS Code 作为主开发环境。

目标

我们希望可以使用 Flutter+Agora Flutter SDK 实现一个简单的视频通话应用,这个视频通话应用需要包含以下功能,

加入通话房间

视频通话

前后摄像头切换

本地静音/取消静音

声网的视频通话是按通话房间区分的,同一个通话房间内的用户都可以互通。为了方便区分,这个演示会需要一个简单的表单页面让用户提交选择加入哪一个房间。同时一个房间内可以容纳最多 4 个用户,当用户数不同时我们需要展示不同的布局。

想清楚了?动手撸代码了。

项目创建

首先在 VS Code 选择查看->命令面板(或直接使用 cmd + shift + P)调出命令面板,输入 flutter 后选择Flutter: New Project创建一个新的 Flutter 项目,项目的名字为agora_flutter_quickstart,随后等待项目创建完成即可。

现在执行启动->启动调试(或 F5)即可看到一个最简单的计数 App。

看起来我们有了一个很好的开始

接下去我们需要对我们新建的项目做一下简单的配置以使其可以引用和使用 Agora Flutter SDK。

打开项目根目录下的 pubspec.yaml 文件,在dependencies下添加agora_rtc_engine:^0.9.0,

dependencies:

flutter:

sdk: flutter

# The following adds the Cupertino Icons font to your application.

# Use with the CupertinoIcons class for iOS style icons.

cupertino_icons: ^0.1.2

# add agora rtc sdk

agora_rtc_engine: ^0.9.0

dev_dependencies:

flutter_test:

sdk: flutter

保存后 VS Code 会自动执行flutter packages get更新依赖。

应用首页

在项目配置完成后,我们就可以开始开发了。首先我们需要创建一个页面文件替换掉默认示例代码中的MyHomePage类。我们可以在lib/src下创建一个pages目录,并创建一个index.dart文件。

如果你已经完成了官方教程 Write your first Flutter app,那么以下代码对你来说就应该不难理解。

classIndexPageextendsStatefulWidget {

@override

State createState() {

returnnewIndexState();

}

}

classIndexStateextendsState {

@override

Widget build(BuildContext context) {

// UI

}

onJoin() {

//TODO

}

}

现在我们需要开始在build方法中构造首页的 UI。

按上图分解 UI 后,我们可以将我们的首页代码修改如下

@override

Widget build(BuildContext context) {

returnScaffold(

appBar: AppBar(

title: Text('Agora Flutter QuickStart'),

),

body: Center(

child: Container(

padding: EdgeInsets.symmetric(horizontal: 20),

height: 400,

child: Column(

children: [

Row(children: []),

Row(children: [

Expanded(

child: TextField(

decoration: InputDecoration(

border: UnderlineInputBorder(

borderSide: BorderSide(width: 1)),

hintText: 'Channel name'),

))

]),

Padding(

padding: EdgeInsets.symmetric(vertical: 20),

child: Row(

children: [

Expanded(

child: RaisedButton(

onPressed: () => onJoin(),

child: Text("Join"),

color: Colors.blueAccent,

textColor: Colors.white,

),

)

],

))

],

)),

));

}

执行 F5 启动查看,应该可以看到下图

看起来不错!但也只是看起来不错。我们的UI现在只能看,还不能交互。我们希望可以基于现在的 UI 实现以下功能,

1. 为 Join 按钮添加回调导航到通话页面

2. 对频道名做检查,若尝试加入频道时频道名为空,则在 TextField 上提示错误

TextField 输入校验

TextField 自身提供了一个decoration属性,我们可以提供一个InputDecoration的对象来标识 TextField 的装饰样式。InputDecoration里的errorText属性非常适合在我们这里被拿来使用, 同时我们利用TextEditingController对象来记录 TextField 的值,以判断当前是否应该显示错误。因此经过简单的修改后,我们的 TextField 代码就变成了这样:

final _channelController = TextEditingController();

/// if channel textfield is validated to have error

bool _validateError = false;

@override

void dispose() {

// dispose input controller

_channelController.dispose();

super.dispose();

}

@override

Widget build(BuildContext context) {

...

TextField(

controller: _channelController,

decoration: InputDecoration(

errorText: _validateError

? "Channel name is mandatory"

: null,

border: UnderlineInputBorder(

borderSide: BorderSide(width: 1)),

hintText: 'Channel name'),

))

...

}

onJoin() {

// update input validation

setState(() {

_channelController.text.isEmpty

? _validateError = true

: _validateError = false;

});

}

在点击加入频道按钮的时候回触发onJoin回调,回调中会先通过setState更新 TextField 的状态以做组件重绘。

注意: 不要忘了 overridedispose方法在这个组件的生命周期结束时释放_controller。

前往通话页面

到这里我们的首页基本就算完成了,最后我们在onJoin中创建MaterialPageRoute将用户导航到通话页面,在这里我们将获取的频道名作为通话页面构造函数的参数传递到下一个页面CallPage。

import'./call.dart';

classIndexStateextendsState {

...

onJoin() {

// update input validation

setState(() {

_channelController.text.isEmpty

? _validateError = true

: _validateError = false;

});

if (_channelController.text.isNotEmpty) {

// push video page with given channel name

Navigator.push(

context,

MaterialPageRoute(

builder: (context) => newCallPage(

channelName: _channelController.text,

)));

}

}

通话页面

同样在/lib/src/pages目录下,我们需要新建一个call.dart文件,在这个文件里我们会实现我们最重要的实时视频通话逻辑。首先还是需要创建我们的CallPage类。如果你还记得我们在IndexPage的实现,CallPage会需要在构造函数中带入一个参数作为频道名。

classCallPageextendsStatefulWidget {

/// non-modifiable channel name of the page

finalString channelName;

/// Creates a call page with given channel name.

constCallPage({Key key, this.channelName}) : super(key: key);

@override

_CallPageState createState() {

returnnew_CallPageState();

}

}

class_CallPageStateextendsState {

@override

Widget build(BuildContext context) {

returnScaffold(

appBar: AppBar(

title: Text(widget.channelName),

),

backgroundColor: Colors.black,

body: Center(

child: Stack(

children: [],

)));

}

}

这里需要注意的是,我们并不需要把参数在创建state实例的时候传入,state可以直接访问widget.channelName获取到组件的属性。

引入声网SDK

因为我们在最开始已经在pubspec.yaml中添加了agora_rtc_engine的依赖,因此我们现在可以直接通过以下方式引入声网 SDK。

import'package:agora_rtc_engine/agora_rtc_engine.dart';

引入后即可以使用创建声网媒体引擎实例。在使用声网 SDK 进行视频通话之前,我们需要进行以下初始化工作。初始化工作应该在整个页面生命周期中只做一次,因此这里我们需要 overrideinitState方法,在这个方法里做好初始化。

class_CallPageStateextendsState {

@override

void initState() {

super.initState();

initialize();

}

void initialize() {

_initAgoraRtcEngine();

_addAgoraEventHandlers();

}

/// Create agora sdk instance and initialze

void _initAgoraRtcEngine() {

AgoraRtcEngine.create(APP_ID);

AgoraRtcEngine.enableVideo();

}

/// Add agora event handlers

void _addAgoraEventHandlers() {

AgoraRtcEngine.onError = (int code) {

// sdk error

};

AgoraRtcEngine.onJoinChannelSuccess =

(String channel, int uid, int elapsed) {

// join channel success

};

AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {

// there's a new user joining this channel

};

AgoraRtcEngine.onUserOffline = (int uid, int reason) {

// there's an existing user leaving this channel

};

}

}

注意:有关如何获取声网 APP_ID,请访问 docs.gora.io 参阅官方文档。

在以上的代码中我们主要创建了声网的媒体 SDK 实例并监听了关键事件,接下去我们会开始做视频流的处理。

在一般的视频通话中,对于本地设备来说一共会有两种视频流,本地流与远端流 - 前者需要通过本地摄像头采集渲染并发送出去,后者需要接收远端流的数据后渲染。现在我们需要动态地将最多4人的视频流渲染到通话页面。

我们会以大致这样的结构渲染通话页面。

这里和首页不同的是,放置通话操作按钮的工具栏是覆盖在视频上的,因此这里我们会使用Stack组件来放置层叠组件。

为了更好地区分 UI 构建,我们将视频构建与工具栏构建分为两个方法。

本地流创建与渲染

要渲染本地流,需要在初始化 SDK 完成后创建一个供视频流渲染的容器,然后通过 SDK 将本地流渲染到对应的容器上。声网 SDK 提供了createNativeView的方法以创建容器,在获取到容器并且成功渲染到容器视图上后,我们就可以利用SDK加入频道与其他客户端互通了。

void initialize() {

_initAgoraRtcEngine();

_addAgoraEventHandlers();

// use _addRenderView everytime a native video view is needed

_addRenderView(0, (viewId) {

// local view setup & preview

AgoraRtcEngine.setupLocalVideo(viewId, 1);

AgoraRtcEngine.startPreview();

// state can access widget directly

AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);

});

}

/// Create a native view and add a new video session object

/// The native viewId can be used to set up local/remote view

void _addRenderView(int uid, Function(int viewId) finished) {

Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) {

setState(() {

_getVideoSession(uid).viewId = viewId;

if (finished != null) {

finished(viewId);

}

});

});

VideoSession session = VideoSession(uid, view);

_sessions.add(session);

}

注意:代码最后利用 uid 与容器信息创建了一个VideoSession对象并添加到_sessions中,这主要是为了视频布局需要,这块稍后会详细触及。

远端流监听与渲染

远端流的监听其实我们已经在前面的初始化代码中提及了,我们可以监听 SDK 提供的onUserJoined与onUserOffline回调来判断是否有其他用户进出当前频道,若有新用户加入频道,就为他创建一个渲染容器并做对应的渲染;若有用户离开频道,则去掉他的渲染容器。

AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {

setState(() {

_addRenderView(uid, (viewId) {

AgoraRtcEngine.setupRemoteVideo(viewId, 1, uid);

});

});

};

AgoraRtcEngine.onUserOffline = (int uid, int reason) {

setState(() {

_removeRenderView(uid);

});

};

/// Remove a native view and remove an existing video session object

void _removeRenderView(int uid) {

VideoSession session = _getVideoSession(uid);

if (session != null) {

_sessions.remove(session);

}

AgoraRtcEngine.removeNativeView(session.viewId);

}

注意:_sessions的作用是在本地保存一份当前频道内的视频流列表信息。因此在用户加入的时候,需要创建对应的VideoSession对象并添加到sessions,在用户离开的时候,则需要删除对应的VideoSession实例。

视频流布局

在有了_sessions数组,且每一个本地/远端流都有了一个对应的原生渲染容器后,我们就可以开始对视频流进行布局了。

/// Helper function to get list of native views

List _getRenderViews() {

return _sessions.map((session) => session.view).toList();

}

/// Video view wrapper

Widget _videoView(view) {

returnExpanded(child: Container(child: view));

}

/// Video view row wrapper

Widget _expandedVideoRow(List views) {

List wrappedViews =

views.map((Widget view) => _videoView(view)).toList();

returnExpanded(

child: Row(

children: wrappedViews,

));

}

/// Video layout wrapper

Widget _viewRows() {

List views = _getRenderViews();

switch (views.length) {

case1:

returnContainer(

child: Column(

children: [_videoView(views[0])],

));

case2:

returnContainer(

child: Column(

children: [

_expandedVideoRow([views[0]]),

_expandedVideoRow([views[1]])

],

));

case3:

returnContainer(

child: Column(

children: [

_expandedVideoRow(views.sublist(0, 2)),

_expandedVideoRow(views.sublist(2, 3))

],

));

case4:

returnContainer(

child: Column(

children: [

_expandedVideoRow(views.sublist(0, 2)),

_expandedVideoRow(views.sublist(2, 4))

],

));

default:

}

returnContainer();

}

工具栏(挂断、静音、切换摄像头)

在实现完视频流布局后,我们接下来实现视频通话的操作工具栏。工具栏里有三个按钮,分别对应静音、挂断、切换摄像头的顺序。用简单的flexRow布局即可。

/// Toolbar layout

Widget _toolbar() {

returnContainer(

alignment: Alignment.bottomCenter,

padding: EdgeInsets.symmetric(vertical: 48),

child: Row(

mainAxisAlignment: MainAxisAlignment.center,

children: [

RawMaterialButton(

onPressed: () => _onToggleMute(),

child: newIcon(

muted ? Icons.mic : Icons.mic_off,

color: muted ? Colors.white : Colors.blueAccent,

size: 20.0,

),

shape: newCircleBorder(),

elevation: 2.0,

fillColor: muted?Colors.blueAccent : Colors.white,

padding: constEdgeInsets.all(12.0),

),

RawMaterialButton(

onPressed: () => _onCallEnd(context),

child: newIcon(

Icons.call_end,

color: Colors.white,

size: 35.0,

),

shape: newCircleBorder(),

elevation: 2.0,

fillColor: Colors.redAccent,

padding: constEdgeInsets.all(15.0),

),

RawMaterialButton(

onPressed: () => _onSwitchCamera(),

child: newIcon(

Icons.switch_camera,

color: Colors.blueAccent,

size: 20.0,

),

shape: newCircleBorder(),

elevation: 2.0,

fillColor: Colors.white,

padding: constEdgeInsets.all(12.0),

)

],

),

);

}

void _onCallEnd(BuildContext context) {

Navigator.pop(context);

}

void _onToggleMute() {

setState(() {

muted = !muted;

});

AgoraRtcEngine.muteLocalAudioStream(muted);

}

void _onSwitchCamera() {

AgoraRtcEngine.switchCamera();

}

最终整合

现在两个部分的 UI 都完成了,我们接下去要将这两个组件通过Stack组装起来。

@override

Widget build(BuildContext context) {

returnScaffold(

appBar: AppBar(

title: Text(widget.channelName),

),

backgroundColor: Colors.black,

body: Center(

child: Stack(

children: [_viewRows(), _toolbar()],

)));

清理

若只在当前页面使用声网 SDK,则需要在离开前调用destroy接口将 SDK 实例销毁。若需要跨页面使用,则推荐将 SDK 实例做成单例以供不同页面访问。同时也要注意对原生渲染容器的释放,可以至直接使用removeNativeView方法释放对应的原生容器。

@override

void dispose() {

// clean up native views & destroy sdk

_sessions.forEach((session) {

AgoraRtcEngine.removeNativeView(session.viewId);

});

_sessions.clear();

AgoraRtcEngine.destroy();

super.dispose();

}

最终效果:

总结

Flutter 作为新生事物,难免还是有他不成熟的地方,但我们已经从他现在的进步上看到了巨大的潜力。从目前的体验来看,只要有充足的社区资源,在 Flutter 上开发跨平台应用还是比较舒服的。声网提供的 Flutter SDK 基本已经覆盖了原生 SDK 提供的大部分方法,开发体验基本可以和原生 SDK 开发保持一致。这次也是基于学习的态度写下了这篇文章,希望对于想要使用 Flutter 开发 RTC 应用的同学有所帮助。

文中讲解的完整代码及 Agora Flutter SDK 可在 Github 获取。

Agora Flutter SDK:

https://github.com/AgoraIO/Flutter-SDK

Quickstart Demo:

https://github.com/AgoraIO-Community/Agora-Flutter-Quickstart

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 视频通话
    +关注

    关注

    0

    文章

    49

    浏览量

    11608

原文标题:构建你的第一个Flutter视频通话应用

文章出处:【微信号:shengwang-agora,微信公众号:声网Agora】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于ARM的视频监控系统的设计

    提出一种基于ARM 嵌入式开发平台视频监控的实现方案。通过V4L2 在Linux 下构建视频图像采集和显示,然后使用servfox 和spcaview 构建
    发表于 11-05 01:56 2783次阅读
    基于ARM的<b class='flag-5'>视频</b>监控系统的设计

    移动视频语音通话/全平台视频监控/视频直播源码出售

    移动视频语音通话/全平台视频监控/视频直播源码出售移动视频
    发表于 03-19 15:53

    移动视频语音通话/全平台视频监控/视频直播源码出售

    移动视频语音通话/全平台视频监控/视频直播源码出售移动视频
    发表于 03-23 09:41

    移动视频语音通话/全平台视频监控/视频直播源码出售

    移动视频语音通话/全平台视频监控/视频直播源码出售移动视频
    发表于 03-31 09:30

    移动视频语音通话/全平台视频监控/视频直播源码出售

    移动视频语音通话/全平台视频监控/视频直播源码出售移动视频
    发表于 05-16 20:59

    移动视频语音通话/全平台视频监控/视频直播源码转让

    与android进行视频通话手机目前是320 以及640的分辨率,如果有需求可扩到更大。pc核心部分使用c++编写, 服务端使用c#系统功能十分稳定,和开源的sip 例如freeswitch等 稳定不是
    发表于 05-18 13:09

    开发者福音——小e智能硬件开发平台视频体验教程

    小e开发平台视频教http://v.youku.com/v_show/id_XMTQzNjE4NTk4MA==.html?qq-pf-to=pcqq.c2c
    发表于 01-05 19:13

    pc安卓ios点对点视频及多人视频语音源码/移动单兵视频源码

    pc安卓ios点对点视频及多人视频语音源码/移动单兵视频源码"点对点视频通话/多人语音
    发表于 06-29 16:47

    平台的Qt调试作简单的介绍

    Qt自带调试功能,如果是主机上的程序可以直接调试,非常方便。同时,Qt是平台的编程语言,在主机上调试其他平台上的程序时就需要相应的设置
    发表于 12-14 08:46

    如何用套代码运行多操作系统应用

    叫做平台平台框架,就是应用的开发框架,开发者基于这个框架开发的应用,可以分别在不同的平台
    发表于 02-08 14:46

    视频教程】紫光同创PDS软件开发平台视频教程之IP使用及仿真

    视频教程】紫光同创PDS软件开发平台视频教程之IP使用及仿真~
    发表于 06-13 15:06

    采用P2P技术为移动电话提供视频通话能力

    采用P2P技术为移动电话提供视频通话能力 IP多媒体处理解决方案供应商Global IP Solutions 公司 (GIPS) 宣布在Windows Mobile平台上提供 Vid
    发表于 10-28 08:54 1924次阅读

    i.MX51平台视频硬件解码的研究与应用

    i.MX51平台视频硬件解码的研究与应用
    发表于 09-23 16:59 29次下载

    中国联通正式开启边缘业务平台视频转码及智能分析系统

    中国联通日前发布公告,正式委托中国信息通信研究院(中国泰尔实验室)开启边缘业务平台视频转码及智能分析系统测试,测试内容及结果将作为后续"2018-2019年中国联通边缘业务平台视频转码及智能分析系统
    发表于 12-20 10:40 1351次阅读

    基于WebAssembly构建Web端音视频通话引擎

    Web技术在发展,音视频通话需求在演进,怎么去实现新的Web技术点在实际应用中的值,以及给我们带来更大的收益是需要我们去探索和实践的。LiveVideoStackCon 2022北京站邀请到田建华
    的头像 发表于 06-26 15:56 484次阅读
    基于WebAssembly<b class='flag-5'>构建</b>Web端音<b class='flag-5'>视频</b><b class='flag-5'>通话</b>引擎