如何搭建一个 Flutter 视频通话应用


随着 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,生成一个临时令牌。然后,在应用中创建两个全局变量,分别命名为 appIdtoken,并把它们设置为从声网读取到的值。

注意:本项目是为了给大家提供参考,处于开发环境,并非生产环境项目。我们建议所有在生产环境中运行的 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 微件的内部展示一个简单的 StackStack 的底层是另一个用户的实时摄像头的大视图,左上角是当前用户的摄像头,这与其他视频通话应用程序的布局相似。为显示当前用户的视频,我们会使用 RtcLocalView.SurfaceView(),这是从设置的输入中获取的,另外,从 RtcRemoteView.SurfaceView(uid: _remoteUid) 中获取远程用户的视频,并向 RtcRemoteView.SurfaceView(uid: _remoteUid) 传入特定用户摄像头的 uid。

为显示通话数据,我们将使用 ScaffoldfloatingActionButton 参数来显示文本为“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
推荐阅读
相关专栏
SDK 教程
164 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。