关于flutter:基于声网-Flutter-SDK-实现互动直播

互动直播是实现很多热门场景的根底,例如直播带货、秀场直播,还有相似抖音的直播 PK等。本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第二篇,他将带着大家用一个小时,利用声网 Flutter 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 文件增加 NSCameraUsageDescription和 NSCameraUsageDescription 的权限申明,或者在 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进行轮循展现后移除。

互动开发

后面咱们曾经实现了直播的简略 Demo 成果,接下来就是实现「互动」的思路了。

后面咱们初始化时注册了一个 onStreamMessage 的回调,能够用于主播和观众之间的音讯互动,那么接下来次要通过两个「互动」成果来展现如果利用声网 SDK 实现互动的能力。

首先是「音讯互动」:

咱们须要通过 SDK 的createDataStream 办法失去一个streamId
而后把要发送的文本内容转为Uint8List
最初利用sendStreamMessage 就能够联合streamId 就能够将内容发送到直播间

streamId = await _engine.createDataStream(
    const DataStreamConfig(syncWithAudio: false, ordered: false));

final data = Uint8List.fromList(
                          utf8.encode(messageController.text));

await _engine.sendStreamMessage(
                        streamId: streamId, data: data, length: data.length);

在 onStreamMessage 里咱们能够通过utf8.decode(data) 失去用户发送的文本内容,联合收到的用户 id ,依据内容,咱们就能够失去如下图所示的互动音讯列表。

onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
    Uint8List data, int length, int sentTs) {
  var message = utf8.decode(data);
  doMessage(remoteUid, message);
}));

后面显示的 id ,前面对应的是用户发送的文本内容

那么咱们再进阶一下,收到用户一些「非凡格局音讯」之后,咱们能够展现动画成果而不是文本内容,例如:

在收到 [*] 格局的音讯时弹出一个动画,相似粉丝送礼。

实现这个成果咱们能够引入第三方 rive 动画库,这个库只有通过 RiveAnimation.network 就能够实现近程加载,这里咱们间接援用一个社区凋谢的收费 riv 动画,并且在弹出后 3s 敞开动画。

showAnima() {
    showDialog(
        context: context,
        builder: (context) {
          return const Center(
            child: SizedBox(
              height: 300,
              width: 300,
              child: RiveAnimation.network(
                'https://public.rive.app/community/runtime-files/4037-8438-first-animation.riv',
              ),
            ),
          );
        },
        barrierColor: Colors.black12);
    Future.delayed(const Duration(seconds: 3), () {
      Navigator.of(context).pop();
    });
  }

最初,咱们通过一个简略的正则判断,如果收到 [*] 格局的音讯就弹出动画,如果是其余就显示文本内容,最终成果如下图动图所示。



bool isSpecialMessage(message) {
  RegExp reg = RegExp(r"[*]$");
  return reg.hasMatch(message);
}

doMessage(int id, String message) {
  if (isSpecialMessage(message) == true) {
    showAnima();
  } else {
    normalMessage(id, message);
  }
}

尽管代码并不非常谨严,然而他展现了如果应用声网 SDK 实现 「互动」的成果,能够看到应用声网 SDK 只须要简略配置就能实现「直播」和 「互动」两个需要场景。
残缺代码如下所示,这外面除了声网 SDK 还引入了另外两个第三方包:

  • flutter_swiper_view 实现用户进入时的循环播放提醒
  • rive用于下面咱们展现的动画成果
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
import 'package:rive/rive.dart';

const token = "xxxxxx";
const cid = "test";
const appId = "xxxxxx";

class LivePage extends StatefulWidget {
  final int uid;
  final int? remoteUid;
  final String type;

  const LivePage(
      {required this.uid, required this.type, this.remoteUid, Key? key})
      : super(key: key);

  @override
  State<StatefulWidget> createState() => _State();
}

class _State extends State<LivePage> {
  late final RtcEngine _engine;
  bool _isReadyPreview = false;

  bool isJoined = false;
  Set<int> remoteUid = {};
  final List<String> _joinTip = [];
  List<Map<int, String>> messageList = [];

  final messageController = TextEditingController();
  final messageListController = ScrollController();
  late VideoViewController rtcController;
  late int streamId;

  final animaStream = StreamController<String>();

  @override
  void initState() {
    super.initState();
    animaStream.stream.listen((event) {
      showAnima();
    });
    _initEngine();
  }

  @override
  void dispose() {
    super.dispose();
    animaStream.close();
    _dispose();
  }

  Future<void> _dispose() async {
    await _engine.leaveChannel();
    await _engine.release();
  }

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

    _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);
          var tip = (widget.type == "Create")
              ? "$rUid 来了"
              : "${connection.localUid} 来了";
          _joinTip.add(tip);
          Future.delayed(const Duration(milliseconds: 1500), () {
            _joinTip.remove(tip);
            setState(() {});
          });
          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) {
          var message = utf8.decode(data);
          doMessage(remoteUid, message);
        }));

    _engine.enableVideo();
    await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
        bitrate: 0,
        mirrorMode: VideoMirrorModeType.videoMirrorModeEnabled,
      ),
    );

    /// 本人直播才须要预览
    if (widget.type == "Create") {
      await _engine.startPreview();
    }

    await _joinChannel();

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

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

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

    streamId = await _engine.createDataStream(
        const DataStreamConfig(syncWithAudio: false, ordered: false));
  }

  bool isSpecialMessage(message) {
    RegExp reg = RegExp(r"[*]$");
    return reg.hasMatch(message);
  }

  doMessage(int id, String message) {
    if (isSpecialMessage(message) == true) {
      animaStream.add(message);
    } else {
      normalMessage(id, message);
    }
  }

  normalMessage(int id, String message) {
    messageList.add({id: message});
    setState(() {});
    Future.delayed(const Duration(seconds: 1), () {
      messageListController
          .jumpTo(messageListController.position.maxScrollExtent + 2);
    });
  }

  showAnima() {
    showDialog(
        context: context,
        builder: (context) {
          return const Center(
            child: SizedBox(
              height: 300,
              width: 300,
              child: RiveAnimation.network(
                'https://public.rive.app/community/runtime-files/4037-8438-first-animation.riv',
              ),
            ),
          );
        },
        barrierColor: Colors.black12);
    Future.delayed(const Duration(seconds: 3), () {
      Navigator.of(context).pop();
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_isReadyPreview) return Container();
    return Scaffold(
      appBar: AppBar(
        title: const Text("LivePage"),
      ),
      body: Column(
        children: [
          Expanded(
            child: 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),
                          ),
                        ),
                      )),
                    ),
                  ),
                ),
                Align(
                  alignment: Alignment.bottomLeft,
                  child: Container(
                    height: 200,
                    width: 150,
                    decoration: const BoxDecoration(
                      borderRadius:
                          BorderRadius.only(topRight: Radius.circular(8)),
                      color: Colors.black12,
                    ),
                    padding: const EdgeInsets.only(left: 5, bottom: 5),
                    child: Column(
                      children: [
                        Expanded(
                          child: ListView.builder(
                            controller: messageListController,
                            itemBuilder: (context, index) {
                              var item = messageList[index];
                              return Padding(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 10, vertical: 10),
                                child: Row(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      item.keys.toList().toString(),
                                      style: const TextStyle(
                                          fontSize: 12, color: Colors.white),
                                    ),
                                    const SizedBox(
                                      width: 10,
                                    ),
                                    Expanded(
                                      child: Text(
                                        item.values.toList()[0],
                                        style: const TextStyle(
                                            fontSize: 12, color: Colors.white),
                                      ),
                                    )
                                  ],
                                ),
                              );
                            },
                            itemCount: messageList.length,
                          ),
                        ),
                        Container(
                          height: 40,
                          color: Colors.black54,
                          padding: const EdgeInsets.only(left: 10),
                          child: Swiper(
                            itemBuilder: (context, index) {
                              return Container(
                                alignment: Alignment.centerLeft,
                                child: Text(
                                  _joinTip[index],
                                  style: const TextStyle(
                                      color: Colors.white, fontSize: 14),
                                ),
                              );
                            },
                            autoplayDelay: 1000,
                            physics: const NeverScrollableScrollPhysics(),
                            itemCount: _joinTip.length,
                            autoplay: true,
                            scrollDirection: Axis.vertical,
                          ),
                        ),
                      ],
                    ),
                  ),
                )
              ],
            ),
          ),
          Container(
            height: 80,
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        isDense: true,
                      ),
                      controller: messageController,
                      keyboardType: TextInputType.number),
                ),
                TextButton(
                    onPressed: () async {
                      if (isSpecialMessage(messageController.text) != true) {
                        messageList.add({widget.uid: messageController.text});
                      }
                      final data = Uint8List.fromList(
                          utf8.encode(messageController.text));
                      await _engine.sendStreamMessage(
                          streamId: streamId, data: data, length: data.length);
                      messageController.clear();
                      setState(() {});
                      // ignore: use_build_context_synchronously
                      FocusScope.of(context).requestFocus(FocusNode());
                    },
                    child: const Text("Send"))
              ],
            ),
          ),
        ],
      ),
    );
  }
}

总结

从下面能够看到,其实跑完根底流程很简略,回顾一下后面的内容,总结下来就是:

  • 申请麦克风和摄像头权限
  • 创立和通过App ID初始化引擎
  • 注册RtcEngineEventHandler回调用于判断状态和接管互动能力
  • 杜绝角色关上和配置视频编码反对
  • 调用joinChannel 退出直播间
  • 通过AgoraVideoView和VideoViewController用户画面
  • 通过engine创立和发送stream音讯
  • 从申请账号到开发 Demo ,利用声网的 SDK 开发一个「互动直播」从需要到实现大略只过了一个小时,尽管上述实现的性能和成果还很毛糙,然而主体流程很快能够跑通了。

同时在 Flutter 的加持下,代码能够在挪动端和 PC 端失去复用,这对于有音视频需要的中小型团队来说无疑是最优组合之一。


欢送开发者们也尝试体验声网 SDK,实现实时音视频互动场景。现注册声网账号下载 SDK,可取得每月收费 10000 分钟应用额度。如在开发过程中遇到疑难,可在声网开发者社区与官网工程师交换。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理