使用Agora Flutter SDK加入多个频道

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

项目介绍

靠自己搭建包含RTE的软件会很繁琐:维护服务器、负载均衡,同时提供优秀的低延迟。

那么,如何才能在节约时间的前提下,将 RTE 添加到 Flutter 应用中?你可以通过 Agora RTE SDK 来提供所有平台的支持。在本教程中,我将带您了解使用Agora Flutter SDK 订阅多个频道的过程。

需求

一个Agora开发者账户(参见指南)。

Flutter SDK

VS代码或Android Studio

对Flutter开发的基本了解

为什么 加入多个频道?

在进入正式开发之前,我们先看看为什么有人需要订阅多个频道。

|480xauto

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

项目设置

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

flutter create agora_multi_channel_demo

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

     dependencies:
           flutter:
             sdk: flutter
    
    
           cupertino_icons: ^1.0.0
           agora_rtc_engine: ^3.2.1
           permission_handler: ^5.1.0+2
    

pubspec.yaml

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

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

flutter pub get

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

|223xauto
Project Setup

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

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

login_page.dart

在成功提交频道名称时,会触发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);
lobby_page.dart

注意:该项目是 作为开发环境下的参考 ,不 推荐 用于生产环境。建议在生产环境中运行的所有RTE app都使用 Token鉴权 。关于Agora平台中基于 Token 的身份验证的更多信息,请参考本指南: https://docs.agora.io/cn/Agora%20Platform/token?platform=Flutter

我们看到,在创建一个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() 类的加入频道函数,这个类寻找两个参数:au toSubscribeAudio和autoSubscribeVideo。由于它期望的是一个布尔值,你可以根据你的要求传递对或错。

对于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。

有了这个,你就可以在你的应用程序中添加多个频道。但是,让我们快速看看我们如何创建小组件,以便这些视频可以显示在我们的屏幕上。

我们首先添加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,你已经实现了你自己的直播应用程序。你可以在实时互动获得此应用程序的完整代码。

其他资源

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

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

也欢迎你加入Agora 开发者社区

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

|600xauto