利用Agora Flutter SDK开发多人视频通话APP

|480xauto

在这篇文章中,我们将看到如何使用Agora Flutter SDK实现自己的Flutter多人视频通话APP

先决条件

如果你是Flutter的新手,那么从Flutter官方网站安装Flutter SDK。

项目设置

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

flutter create agora_group_calling

  1. 找到 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

在添加包的时候要注意缩进,因为如果缩进不对,可能会出错。

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

flutter pub get

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

|231xauto
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), ));
HomePage.dart — onJoin()

创建我们的呼叫页面

在我们开始使用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()方法,它可以帮助你实现所需的功能。

|352xauto

结论

现在你已经实现了Flutter多人视频通话APP,使用了Agora Flutter SDK,并实现了一些基本功能,如静音本地视频流、切换摄像头和断开通话。

你可以在声网多人通话应用示例代码得到这个应用程序的完整代码。

其他资源

要了解更多关于Agora Flutter SDK和其他用例的信息,你可以参考这里的开发者指南

您也可以在这里查看上面讨论的功能和其他许多功能的声网Flutter完整文档

获取更多文档、Demo、技术帮助

|600xauto