关于flutter:如何基于-React-Native-快速实现一个视频通话应用

38次阅读

共计 10571 个字符,预计需要花费 27 分钟才能阅读完成。

明天,咱们将会一起开发一个蕴含 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、技术帮忙,请点击「浏览原文」拜访声网开发者社区。

正文完
 0