基于声网 Flutter SDK 实现互动直播(上)

本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第二篇。上一篇介绍了如何实现多人视频互动。本文讲述如何实现互动直播场景,包括视频直播、发评论、送礼物等基础功能。该系列将基于声网 Fluttter SDK 实现视频通话、互动直播,并尝试虚拟背景等更多功能的实现。

开发一个跨平台的的直播的功能需要多久?如果直播还需要支持各种互动效果呢?

我给出的答案是不到一个小时,在 Flutter + 声网 SDK 的加持下,你可以在一个小时之内就完成一个互动直播的雏形。

声网作为最早支持 Flutter 平台的 SDK 厂商之一, 其 RTC SDK 实现主要来自于封装好的 C/C++ 等 native 代码,而这些代码会被打包为对应平台的动态链接库,最后通过 Dart 的 FFI(ffigen) 进行封装调用,减少了 Flutter 和原生平台交互时在 Channel 上的性能开销。

开始之前

接下来让我们进入正题,既然选择了 Flutter + 声网的实现路线,那么在开始之前肯定有一些需要准备的前置条件,首先是为了满足声网 RTC SDK 的使用条件,开发环境必须为:

  • Flutter 2.0 或更高版本
  • Dart 2.14.0 或更高版本

从目前 Flutter 和 Dart 版本来看,上面这个要求并不算高,然后就是你需要注册一个声网开发者账号 ,从而获取后续配置所需的 App ID 和 Token 等配置参数。

可访问这里注册声网账号:https://sso2.agora.io/cn/v4/signup/with-sms
如果对于配置“门清”,可以忽略跳过这部分直接看下一章节。

创建项目

首先可以在声网控制台的项目管理页面上点击创建项目,然后在弹出框选输入项目名称,之后选择「互动直播」场景和「安全模式(APP ID + Token)」 即可完成项目创建。

根据法规,创建项目需要实名认证,这个必不可少,另外使用场景不必太过纠结,项目创建之后也是可以根据需要自己修改。

获取App ID

在项目列表点击创建好的项目配置,进入项目详情页面之后,会看到基本信息栏目有个 App ID 的字段,点击如下图所示图标,即可获取项目的 App ID。

App ID 也算是敏感信息之一,所以尽量妥善保存,避免泄密。

获取 Token

为提高项目的安全性,声网推荐了使用 Token 对加入频道的用户进行鉴权,在生产环境中,一般为保障安全,是需要用户通过自己的服务器去签发 Token,而如果是测试需要,可以在项目详情页面的「临时 token 生成器」获取临时 Token:

在频道名输入一个临时频道,比如 Test2 ,然后点击生成临时 token 按键,即可获取一个临时 Token,有效期为 24 小时。

这里得到的 Token 和频道名就可以直接用于后续的测试,如果是用在生产环境上,建议还是在服务端签发 Token ,签发 Token 除了 App ID 还会用到 App 证书,获取 App 证书同样可以在项目详情的应用配置上获取。

更多服务端签发 Token 可见 token server 文档 。

开始开发

通过前面的配置,我们现在拥有了 App ID、频道名和一个有效的临时 Token ,接下里就是在 Flutter 项目里引入声网的 RTC SDK :agora_rtc_engine 。

项目配置

首先在 Flutter 项目的 pubspec.yaml文件中添加以下依赖,其中 agora_rtc_engine 这里引入的是6.1.0 版本 。

其实 permission_handler 并不是必须的,只是因为视频通话项目必不可少需要申请到麦克风和相机权限,所以这里推荐使用 permission_handler来完成权限的动态申请。

dependencies:
  flutter:
    sdk: flutter

  agora_rtc_engine: ^6.1.0
  permission_handler: ^10.2.0

这里需要注意的是, Android 平台不需要特意在主工程的 AndroidManifest.xml文件上添加uses-permission ,因为 SDK 的 AndroidManifest.xml 已经添加过所需的权限。

iOS和macOS可以直接在 Info.plist 文件添加 NSCameraUsageDescriptionNSCameraUsageDescription 的权限声明,或者在 Xcode 的 Info 栏目添加Privacy - Microphone Usage Description Privacy - Camera Usage Description。

<key>NSCameraUsageDescription</key>
  <string>*****</string>
  <key>NSMicrophoneUsageDescription</key>
  <string>*****</string>

使用声网 SDK

获取权限

在正式调用声网 SDK 的 API 之前,首先我们需要申请权限,如下代码所示,可以使用 permission_handler 的 request 提前获取所需的麦克风和摄像头权限。

@override
void initState() {
  super.initState();

  _requestPermissionIfNeed();
}

Future<void> _requestPermissionIfNeed() async {
  await [Permission.microphone, Permission.camera].request();
}
因为是测试项目,默认我们可以在应用首页就申请获得。

初始化引擎

接下来开始配置 RTC 引擎,如下代码所示,通过 import 对应的 dart 文件之后,就可以通过 SDK 自带的 createAgoraRtcEngine 方法快速创建引擎,然后通过 initialize方法就可以初始化 RTC 引擎了,可以看到这里会用到前面创建项目时得到的 App ID 进行初始化。

注意这里需要在请求完权限之后再初始化引擎。
import 'package:agora_rtc_engine/agora_rtc_engine.dart';

late final RtcEngine _engine;


Future<void> _initEngine() async {
   _engine = createAgoraRtcEngine();
  await _engine.initialize(const RtcEngineContext(
    appId: appId,
  ));
  ···
}

接着我们需要通过 registerEventHandler注册一系列回调方法,在 RtcEngineEventHandler 里有很多回调通知,而一般情况下我们比如常用到的会是下面这几个:

  • onError :判断错误类型和错误信息
  • onJoinChannelSuccess:加入频道成功
  • onUserJoined:有用户加入了频道
  • onUserOffline:有用户离开了频道
  • onLeaveChannel:离开频道
  • onStreamMessage: 用于接受远端用户发送的消息
Future<void> _initEngine() async {
        ···
       _engine.registerEventHandler(RtcEngineEventHandler(
        onError: (ErrorCodeType err, String msg) {},
        onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
          setState(() {
            isJoined = true;
          });
        },
        onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
          remoteUid.add(rUid);
          setState(() {});
        },
        onUserOffline:
            (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
          setState(() {
            remoteUid.removeWhere((element) => element == rUid);
          });
        },
        onLeaveChannel: (RtcConnection connection, RtcStats stats) {
          setState(() {
            isJoined = false;
            remoteUid.clear();
          });
        },
        onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
            Uint8List data, int length, int sentTs) {
       
        }));
用户可以根据上面的回调来判断 UI 状态,比如当前用户时候处于频道内显示对方的头像和数据,提示用户进入直播间,接收观众发送的消息等。

接下来因为我们的需求是「互动直播」,所以就会有观众和主播的概念,所以如下代码所示:

  • 首先需要调用 enableVideo 打开视频模块支持,可以看到视频画面
  • 同时我们还可以对视频编码进行一些简单配置,比如通过
  • VideoEncoderConfiguration 配置分辨率是帧率
  • 根据进入用户的不同,我们假设type为 "Create"是主播, "Join"是观众
  • 那么初始化时,主播需要通过通过startPreview开启预览
  • 观众需要通过enableLocalAudio(false); 和enableLocalVideo(false);关闭本地的音视频效果
Future<void> _initEngine() async {
    ···
    _engine.enableVideo();
    await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
      ),
    );  
    /// 自己直播才需要预览
    if (widget.type == "Create") {
      await _engine.startPreview();
    }

    if (widget.type != "Create") {
      _engine.enableLocalAudio(false);
      _engine.enableLocalVideo(false);
    }

关于 setVideoEncoderConfiguration 的更多参数配置支持如下所示:

接下来需要初始化一个 VideoViewController,根据角色的不同:

主播可以通过VideoViewController直接构建控制器,因为画面是通过主播本地发出的流观众需要通过VideoViewController.remote构建,因为观众需要获取的是主播的信息流,区别在于多了connection 参数需要写入channelId,同时VideoCanvas需要写入主播的uid 才能获取到画面

late VideoViewController rtcController; 
Future<void> _initEngine() async {
   ···
   rtcController = widget.type == "Create"
       ? VideoViewController(
           rtcEngine: _engine,
           canvas: const VideoCanvas(uid: 0),
         )
       : VideoViewController.remote(
           rtcEngine: _engine,
           connection: const RtcConnection(channelId: cid),
           canvas: VideoCanvas(uid: widget.remoteUid),
         );
   setState(() {
     _isReadyPreview = true;
   });


最后调用 joinChannel加入直播间就可以了,其中这些参数都是必须的:

  • token 就是前面临时生成的Token
  • channelId 就是前面的渠道名
  • uid 就是当前用户的id ,这些id 都是我们自己定义的
  • channelProfile根据角色我们可以选择不同的类别,比如主播因为是发起者,可以选择channelProfileLiveBroadcasting ;而观众选择channelProfileCommunication
  • clientRoleType选择clientRoleBroadcaster
Future<void> _initEngine() async {
   ···
   await _joinChannel();
}
Future<void> _joinChannel() async {
  await _engine.joinChannel(
    token: token,
    channelId: cid,
    uid: widget.uid,
    options: ChannelMediaOptions(
      channelProfile: widget.type == "Create"
          ? ChannelProfileType.channelProfileLiveBroadcasting
          : ChannelProfileType.channelProfileCommunication,
      clientRoleType: ClientRoleType.clientRoleBroadcaster,
      // clientRoleType: widget.type == "Create"
      //     ? ClientRoleType.clientRoleBroadcaster
      //     : ClientRoleType.clientRoleAudience,
    ),
  );
之前我以为观众可以选择 clientRoleAudience 角色,但是后续发现如果用户是通过 clientRoleAudience 加入可以直播间,onUserJoined 等回调不会被触发,这会影响到我们后续的开发,所以最后还是选择了 clientRoleBroadcaster。

渲染画面

接下来就是渲染画面,如下代码所示,在 UI 上加入 AgoraVideoView控件,并把上面初始化成功的RtcEngine和VideoViewController配置到 AgoraVideoView,就可以完成画面预览。

Stack(
  children: [
    AgoraVideoView(
      controller: rtcController,
    ),
    Align(
      alignment: const Alignment(-.95, -.95),
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
          children: List.of(remoteUid.map(
            (e) => Container(
              width: 40,
              height: 40,
              decoration: const BoxDecoration(
                  shape: BoxShape.circle, color: Colors.blueAccent),
              alignment: Alignment.center,
              child: Text(
                e.toString(),
                style: const TextStyle(
                    fontSize: 10, color: Colors.white),
              ),
            ),
          )),
        ),
      ),
    ),
这里还在页面顶部增加了一个 SingleChildScrollView ,把直播间里的观众 id 绘制出来,展示当前有多少观众在线。


接着我们只需要在做一些简单的配置,就可以完成一个简单直播 Demo 了,如下图所示,在主页我们提供 Create 和 Join 两种角色进行选择,并且模拟用户的 uid 来进入直播间:

  • 主播只需要输入自己的 uid 即可开播
  • 观众需要输入自己的 uid 的同时,也输入主播的 uid ,这样才能获取到主播的画面


接着我们只需要通过 Navigator.push 打开页面,就可以看到主播(左)成功开播后,观众(右)进入直播间的画面效果了,这时候如果你看下方截图,可能会发现观众和主播的画面是镜像相反的。


如果想要主播和观众看到的画面是一致的话,可以在前面初始化代码的 VideoEncoderConfiguration 里配置 mirrorMode 为 videoMirrorModeEnabled,就可以让主播画面和观众一致。

 await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
        bitrate: 0,
        mirrorMode: VideoMirrorModeType.videoMirrorModeEnabled,
      ),
    );
这里 mirrorMode 配置不需要区分角色,因为 mirrorMode 参数只会只影响远程用户看到的视频效果。

上面动图左下角还有一个观众进入直播间时的提示效果,这是根据 onUserJoined 回调实现,在收到用户进入直播间后,将 id 写入数组,并通过PageView进行轮循展示后移除。



推荐阅读
相关专栏
SDK 教程
167 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。