实现基于声网Web SDK的视频会议的使用体验

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现基于声网Web SDK的视频会议的使用体验相关的知识,希望对你有一定的参考价值。

引导语

众所周知,市面上有比如飞书会议、腾讯会议等实现视频会议功能的应用,而且随着这几年大环境的影响,远程协作办公越来越成为常态,关于视频会议的应用也会越来越多,且在远程办公的沟通协作中对沟通软件的使用要求会越来越严格。正是因为外部大环境的原因直接促进了远程办公从起步逐渐走向成熟,打破了传统的以场地办公为主的模式,这使得视频会议的应用呈现出一派繁荣发展的景象。这些关于视频会议的功能怎么实现呢?那么本文就来聊聊关于视频会议的实现分析,主要通过视频会议的核心两点拆分来看,即虚拟背景实现和AI降噪两个方面,以及基于声网Web SDK的简单使用体验。

使用前的准备工作

由于本文分享的是关于声网Web SDK的视频会议的使用心得,读者如果也想体验声网的这个Web SDK,需要提前准备使用前的工作,具体如下所示:

1、开发环境

其实声网的Web SDK兼容性非常不错,对硬件设备和软件系统的要求不高,开发和测试环境只要满足以下条件即可:

  • Chrome
  • Firefox
  • Safari
  • Edge

2、本文的体验测试使用到的开发环境

  • MacBook Pro
  • Visual Studio Code

3、本文的体验测试使用到的测试环境

  • Chrome

4、其他

  • 在使用声网Web SDK的时候,如果没有声网账号,需要先去注册一个声网账号,然后进入声网后台管理平台创建你要使用的AppID、获取 Token等操作。
  • 提前下载声网官方的关于视频会议的Web SDK。声网官方视频会议SDK下载地址:​https://docs.agora.io/cn/video-call-4.x/downloads?platform=Web​

一、虚拟背景实现

随着视频会议应用的功能不断更新发展,为了迎合实际的用户需求,各大视频会议应用的厂商都推出了个性化的功能。在视频会议应用的使用中,关于虚拟背景的功能就是比较重要的一环,虚拟背景可以说是视频会议应用必备功能,而且虚拟背景对应的功能随着用户的需求变的越来越复杂。

1、过程原理介绍

从技术层面来看,虚拟背景主要是依托于人像分割技术,通过把图片中的人像分割出来,再对背景图片进行替换的操作。从实际的使用情况分为三种:

  • 实时通讯情形:主要是为了保护使用者的隐私,如视频远程会议;
  • 直播情形:主要是为了营造气氛,如技术直播、公司线上年会;
  • 互动娱乐情形:主要是为了增强趣味性,如短视频中人物特性。

2、具体步骤

本文以声网对应的虚拟背景功能的集成使用为例来讲,用到的就是虚拟背景插件agora-extension-virtual-background,然后结合声网的Web SDK搭配使用,可以将使用者人像和背景分离开,虚化使用者的实际背景,或者使用自定义内容来替代实际背景,可以很好的保护使用者隐私,以及避免杂乱的背景对其他观众造成不好的视觉体验。声网关于虚拟背景的技术实现原理也有很清晰的介绍,如下所示:

实现基于声网Web

那么接下来就来看看虚拟背景的功能怎么实现吧,具体使用步骤如下所示:

(1)首先实现音视频功能,初始化下载之后的demo,具体如下所示:

实现基于声网Web

实现基于声网Web

在前端demo中集成声网音视频SDK,主要通过npm来操作,具体操作步骤如下所示:

实现基于声网Web

实现基于声网Web

实现基于声网Web

上面引入之后,需要在使用的项目中导入AgoraRTC 模块,具体如下所示:

实现基于声网Web

上面引入之后,实现客户端的用户使用的界面之后,在具体的地方创建AgoraRTCClient 对象,具体如下所示:

实现基于声网Web

接下来是麦克风采集和摄像头采集的方法,创建本地对应的轨道对象,具体如下所示:

实现基于声网Web

具体的运行效果如下所示:

实现基于声网Web

实现基于声网Web

具体的其他内容,可以参看声网音视频官方文档:​​https://docs.agora.io/cn/video-call-4.x/start_call_web_ng?platform=Web​

实现基于声网Web

(2)在前端demo中引入虚拟背景插件,具体的命令行:

npm install agora-extension-virtual-background

(3)在对应的具体使用的地方引入虚拟背景插件,具体操作:

import VirtualBackgroundExtension from "agora-extension-virtual-background"; //这里示例以通过import的方式来引入

(4)需要注意的一点就是声网的虚拟背景插件依赖与Wasm文件,使用的时候需要把Wasm文件放在CDN或者静态服务器中,本示例只在本地运行,所以无需发布在CDN上,但是实际使用的时候要记得放在CDN上,切记!

(5)在实际的前端页面中实现虚拟背景插件的注册操作,具体代码段:

// 创建 Client
var client = AgoraRTC.createClient(mode: "rtc", codec: "vp8");
// 创建 VirtualBackgroundExtension 实例
const extension = new VirtualBackgroundExtension();
// 检查兼容性
if (!extension.checkCompatibility())
// 当前浏览器不支持虚拟背景插件,可以停止执行之后的逻辑
console.error("该版本暂不支持该功能!");

// 注册插件
AgoraRTC.registerExtensions([extension]);

(6)初始化虚拟背景插件,具体代码段:

// 初始化
async function getProcessorInstance()
processor = extension.createProcessor(); //创建 VirtualBackgroundProcessor 实例。
try
await processor.init("./assets/wasms"); // 初始化插件,传入 Wasm 文件路径
catch(e)
//捕获异常并进行相应处理。
console.log("Fail");
return null;

localTracks.videoTrack.pipe(processor).pipe(localTracks.videoTrack.processorDestination); // 将插件注入 SDK 内的视频处理管道

(7)通过processor.setOptions()方法设置虚拟背景类型和对应的参数,示例:

processor.setOptions(type: color, color: #FF0000); //type表示背景类型为颜色;对应的是颜色色值的参数。(其实还有img、blur、video等类型,这里不在一一列举)

(8)打开虚拟背景功能,通过processor.enable()方法:

await processor.enable();

(9)短暂关闭虚拟背景功能,通过processor.disable()方法:

processor.disable();

(10)结束关闭虚拟背景功能,通过videoTrack.unpipe()方法:

localTracks.videoTrack.unpipe();

3、小结

通过上面关于引入虚拟背景的核心步骤,可以看到声网的虚拟背景插件使用起来非常简单,只需简单的几步,就可以在前端音视频项目中实现虚拟背景的功能,而且关于虚拟背景的虚拟效果有很多个选项,完全可以满足实际使用中的需求。个人觉得声网这个虚拟背景插件非常好用,不仅集成使用很简单,而且实现的效果也很不错,感兴趣的读者快来下手试一下吧!

二、AI降噪实现

在视频会议应用的使用中,另外一个比较重要的环节就是降噪。因为在实际的线上会议中,如果参会人员都是处在比较安静的环境下还好说,但是一般在线上会议的时候参会人员所处的环境都不相同,且所处的环境会有各种噪音,往往这些噪音会直接降低在线会议中的音质,从而影响会议的体验。所以通过使用降噪,就可以把在线会议过程中的噪音去掉,进而提高参会人员的良好体验。

1、过程原理介绍

依然从技术层面来看,降噪其实就是获取音频信号并且消除音频中的噪音的过程。由于声音是由空气中的压力波组成,关于人所能感知到的实际声音只是一小部分,其中还包括各种回声、噪音、周围的其他环境音,声网推出的关于降噪的功能:基于AI降噪,通过使用AI降噪可以解决上述的痛点问题。

2、具体步骤

这里还是以声网对应的AI降噪功能的集成使用为例来讲,用到的就是AI降噪插件agora-extension-ai-denoiser,然后结合声网的Web SDK搭配使用,可以降低上百种噪声,减少多人同时说话时的人声失真等问题。对于在线会议、语聊房、远程问诊、游戏语音等场景,AI 降噪插件能够让远程交流和面对面交谈一样实时。声网关于AI降噪的技术实现原理也有很清晰的介绍,如下所示:

实现基于声网Web

那么接下来就来看看AI降噪的功能怎么实现吧,具体使用步骤如下所示:

(1)首先还是要实现音视频功能,具体步骤同虚拟背景实现的步骤(1);

(2)在前端demo中引入AI降噪插件,具体的命令行:

npm install agora-extension-ai-denoiser

(3)在项目.js 文件中加入导入 AI 降噪模块,具体操作:

import AIDenoiserExtension from "agora-extension-ai-denoiser";

(4)还是要注意的是AI降噪插件依赖与Wasm文件,使用的时候需要把Wasm文件放在CDN或者静态服务器中,本示例只在本地运行,所以无需发布在CDN上,但是实际使用的时候要记得放在CDN上,切记!

(5)在实际的前端页面中实现AI降噪景插件的注册操作,具体代码段:

const denoiser = new AIDenoiserExtension(assetsPath:./external); //注意路径结尾不带 “/”
// 检查兼容性
if (!denoiser.checkCompatibility())
// 当前浏览器可能不支持 AI 降噪插件,可以停止执行之后的逻辑
console.error("该版本暂不支持该功能!");

// 注册插件
AgoraRTC.registerExtensions([denoiser]);
denoiser.onloaderror = (e) =>
//捕获异常并进行相应处理。
console.log(e);

(6)创建实例,具体代码段:

const processor = denoiser.createProcessor();  // 创建 processor
processor.enable(); // 设置插件为默认开启

或者

processor.disable(); // 设置插件为默认关闭

(7)把AI降噪插件注入到音频处理管道中,具体代码段:

const audioTrack = await AgoraRTC.createMicrophoneAudioTrack();   // 创建音频轨道 
audioTrack.pipe(processor).pipe(audioTrack.processorDestination); // 将插件注入音频处理管道
await processor.enable(); // 设置插件为开启

(8)设置AI降噪的开启或者关闭状态,具体代码段:

if (processor.enabled)  //已经开启状态
await processor.disable(); // 设置插件为关闭
else
await processor.enable(); // 设置插件为开启

(9)调整降噪模式和等级,具体代码段:

// 用来监听降噪处理耗时过长的事件
processor.onoverload = async (elapsedTime) =>
// 调整为稳态降噪模式,临时关闭 AI 降噪
await processor.setMode("STATIONARY_NS");
或者
// 完全关闭 AI 降噪,用浏览器自带降噪
// await processor.disable()

(10)转储降噪处理中的音频数据,具体代码段:

processor.dump(); //调用dump方法

3、小结

通过上面关于引入AI降噪的核心步骤,可以看到声网的AI降噪插件使用起来非常简单,只需简单几步就可在前端音视频项目中实现AI降噪的功能。个人觉得声网这个AI降噪插件同样非常的好用,不仅集成使用很简单,而且实现的效果也很不错,感兴趣的读者快来体验吧!


结束语

通过本文关于实现基于声网Web SDK的视频会议的使用体验,从视频会议的核心部分来做实现分析,是不是可以上手来开发一个属于视频会议应用了呢?声网的虚拟背景实现和AI降噪两个核心功能,不仅使用的步骤很简单,而且实现出来的效果很不错,完全可以满足想要开发视频会议相关应用的需求。整体操作下来,个人还是觉得声网对应的API文档写的太好了,很详细,步骤也很清晰,还有就是声网产品的集成步骤也很简单,节省了集成插件的时间,从集成到调用,再到体验,用了不到一小时就搞定了虚拟背景实现和AI降噪两个模块的体验。所以,有在开发音视频相关的朋友可以看过来了,声网的相关产品不仅成熟,还很好用,快来体验使用吧!

参考文献

  1. 声网文档中心--SDK下载:​​https://docs.agora.io/cn/video-call-4.x/downloads?platform=Web​
  2. Demo下载:​​https://github.com/AgoraIO/API-Examples-Web/tree/main/Demo​
  3. 声网文档中心--虚拟背景文档:​​https://docs.agora.io/cn/video-call-4.x/virtual_background_web_ng?platform=Web​
  4. 声网文档中心--AI 降噪文档:​​https://docs.agora.io/cn/video-call-4.x/noise_reduction_web_ng?platform=Web​

基于声网 Flutter SDK 实现多人视频通话

前言

本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第一篇。本文除了讲述实现多人视频通话的过程,还有一些 Flutter 开发方面的知识点。该系列将基于声网 Fluttter SDK 实现视频通话、互动直播,并尝试虚拟背景等更多功能的实现。


如果你有一个实现 “多人视频通话” 的场景需求,你会选择从零实现还是接第三方 SDK?如果在这个场景上你还需要支持跨平台,你会选择怎么样的技术路线?

我的答案是:Flutter + 声网 SDK,这个组合可以完美解决跨平台和多人视频通话的所有痛点,因为:

  • Flutter 天然支持手机端和 PC 端的跨平台能力,并拥有不错的性能表现

  • 声网的 Flutter RTC SDK 同样支持 Android、iOS、MacOS 和 Windows 等平台,同时也是难得针对 Flutter 进行了全平台支持和优化的音视频 SDK

在开始之前,有必要提前简单介绍一下声网的 RTC SDK 相关实现,这也是我选择声网的原因。

声网属于是国内最早一批做 Flutter SDK 全平台支持的厂家,声网的 Flutter SDK 之所以能在 Flutter 上最早保持多平台的支持,原因在于声网并不是使用常规的 Flutter Channel 去实现平台音视频能力:

声网的 RTC SDK 的逻辑实现都来自于封装好的 C/C++ 等 native 代码,而这些代码会被打包为对应平台的动态链接库,例如.dll、.so 、.dylib ,最后通过 Dart 的 FFI(ffigen) 进行封装调用

这样做的好处在于:

  • Dart 可以和 native SDK 直接通信,减少了 Flutter 和原生平台交互时在 Channel 上的性能开销;
  • C/C++ 相关实现在获得更好性能支持的同时,也不需要过度依赖原生平台的 API ,可以得到更灵活和安全的 API 支持。

如果说这样做有什么坏处,那大概就是 SDK 的底层开发和维护成本会剧增,不过从用户角度来看,这无异是一个绝佳的选择。

开发之前

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

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

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

如果对后续配置“门清”,可以忽略跳过。

创建项目

首先可以在声网控制台的项目管理页面上点击「创建项目」,然后在弹出框选输入项目名称,之后选择「视频通话」场景和「安全模式(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 进行初始化。

注意这里需要在请求完权限之后再初始化引擎,并更新初始化成功状态 initStatus,因为没成功初始化之前不能使用 RtcEngine。

import 'package:agora_rtc_engine/agora_rtc_engine.dart';

late final RtcEngine _engine;

///初始化状态
late final Future<bool?> initStatus;

@override
void initState() 
  super.initState();
  ///请求完成权限后,初始化引擎,更新初始化成功状态
  initStatus = _requestPermissionIfNeed().then((value) async 
    await _initEngine();
    return true;
  ).whenComplete(() => setState(() ));



Future<void> _initEngine() async 
  //创建 RtcEngine
  _engine = createAgoraRtcEngine();
  // 初始化 RtcEngine
  await _engine.initialize(RtcEngineContext(
    appId: appId,
  ));
  ···


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

  • onError :判断错误类型和错误信息
  • onJoinChannelSuccess:加入频道成功
  • onUserJoined:有用户加入了频道
  • onUserOffline:有用户离开了频道
  • onLeaveChannel:离开频道

///是否加入聊天
bool isJoined = false;
/// 记录加入的用户id
Set<int> remoteUid = ;

Future<void> _initEngine() async 
   ···
   _engine.registerEventHandler(RtcEngineEventHandler(
      // 遇到错误
      onError: (ErrorCodeType err, String msg) 
        print('[onError] err: $err, msg: $msg');
      ,
      onJoinChannelSuccess: (RtcConnection connection, int elapsed) 
        // 加入频道成功
        setState(() 
          isJoined = true;
        );
      ,
      onUserJoined: (RtcConnection connection, int rUid, int elapsed) 
        // 有用户加入
        setState(() 
          remoteUid.add(rUid);
        );
      ,
      onUserOffline:
          (RtcConnection connection, int rUid, UserOfflineReasonType reason) 
        // 有用户离线
        setState(() 
          remoteUid.removeWhere((element) => element == rUid);
        );
      ,
      onLeaveChannel: (RtcConnection connection, RtcStats stats) 
        // 离开频道
        setState(() 
          isJoined = false;
          remoteUid.clear();
        );
      ,
    ));


用户可以根据上面的回调来判断 UI 状态,比如当前用户处于频道内时显示对方的头像和数据,其他用户加入和离开频道时更新当前 UI 等。

接下来因为我们的需求是「多人视频通话」,所以还需要调用 enableVideo 打开视频模块支持,同时我们还可以对视频编码进行一些简单配置,比如通过 VideoEncoderConfiguration 配置 :

  • dimensions:配置视频的分辨率尺寸,默认是 640x360
  • frameRate:配置视频的帧率,默认是 15 fps Future _initEngine() async
  Future<void> _initEngine() async 

    ···
    // 打开视频模块支持
    await _engine.enableVideo();
    // 配置视频编码器,编码视频的尺寸(像素),帧率
    await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
      ),
    );

    await _engine.startPreview();
  

更多参数配置支持如下所示:

最后调用 startPreview 开启画面预览功能,接下来只需要把初始化好的 Engine 配置到 AgoraVideoView 控件就可以完成渲染。

渲染画面

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

根据前面的initStatus状态,在_engine初始化成功后才加载 AgoraVideoView。

Scaffold(
  appBar: AppBar(),
  body: FutureBuilder<bool?>(
      future: initStatus,
      builder: (context, snap) 
        if (snap.data != true) 
          return Center(
            child: new Text(
              "初始化ing",
              style: TextStyle(fontSize: 30),
            ),
          );
        
        return AgoraVideoView(
          controller: VideoViewController(
            rtcEngine: _engine,
            canvas: const VideoCanvas(uid: 0),
          ),
        );
      ),
);

这里还有另外一个参数 VideoCanvas ,其中的 uid 是用来标志用户id的,这里因为是本地用户,这里暂时用 0 表示 。

如果需要加入频道,可以调用 joinChannel 方法加入对应频道,以下的参数都是必须的,其中:

  • token 就是前面临时生成的 Token
  • channelId 就是前面的渠道名
  • uid 和上面一样逻辑
  • channelProfile 选择 channelProfileLiveBroadcasting ,因为我们需要的是多人通话。
  • clientRoleType 选择 clientRoleBroadcaster,因为我们需要多人通话,所以我们需要进来的用户可以交流发送内容。
Scaffold(
  appBar: AppBar(),
  body: FutureBuilder<bool?>(
      future: initStatus,
      builder: (context, snap) 
        if (snap.data != true) 
          return Center(
            child: new Text(
              "初始化ing",
              style: TextStyle(fontSize: 30),
            ),
          );
        
        return AgoraVideoView(
          controller: VideoViewController(
            rtcEngine: _engine,
            canvas: const VideoCanvas(uid: 0),
          ),
        );
      ),
);

同样的道理,通过前面的 RtcEngineEventHandler ,我们可以获取到加入频道用户的 uid(rUid) ,所以还是AgoraVideoView,但是我们使用 VideoViewController.remote根据 uid 和频道id去创建 controller ,配合 SingleChildScrollView 在顶部显示一排可以左右滑动的用户小窗效果。

用 Stack 嵌套层级。

Scaffold(
  appBar: AppBar(),
  body: Stack(
    children: [
      AgoraVideoView(
      ·····
      ),
      Align(
        alignment: Alignment.topLeft,
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: List.of(remoteUid.map(
                  (e) =>
                  SizedBox(
                    width: 120,
                    height: 120,
                    child: AgoraVideoView(
                      controller: VideoViewController.remote(
                        rtcEngine: _engine,
                        canvas: VideoCanvas(uid: e),
                        connection: RtcConnection(channelId: channel),
                      ),
                    ),
                  ),
            )),
          ),
        ),
      )
    ],
  ),
);

这里的 remoteUid 就是一个保存加入到 channel 的 uid 的 Set 对象。

最终运行效果如下图所示,引擎加载成功之后,点击 FloatingActionButton 加入,可以看到移动端和PC端都可以正常通信交互,并且不管是通话质量还是画面流畅度都相当优秀,可以感受到声网 SDK 的完成度还是相当之高的。

红色是我自己加上的打码。

在使用该例子测试了 12 人同时在线通话效果,基本和微信视频会议没有差别,以下是完整代码:


class VideoChatPage extends StatefulWidget 
  const VideoChatPage(Key? key) : super(key: key);

  @override
  State<VideoChatPage> createState() => _VideoChatPageState();


class _VideoChatPageState extends State<VideoChatPage> 
  late final RtcEngine _engine;

  ///初始化状态
  late final Future<bool?> initStatus;

  ///是否加入聊天
  bool isJoined = false;

  /// 记录加入的用户id
  Set<int> remoteUid = ;

  @override
  void initState() 
    super.initState();
    initStatus = _requestPermissionIfNeed().then((value) async 
      await _initEngine();
      return true;
    ).whenComplete(() => setState(() ));
  

  Future<void> _requestPermissionIfNeed() async 
    await [Permission.microphone, Permission.camera].request();
  

  Future<void> _initEngine() async 
    //创建 RtcEngine
    _engine = createAgoraRtcEngine();
    // 初始化 RtcEngine
    await _engine.initialize(RtcEngineContext(
      appId: appId,
    ));

    _engine.registerEventHandler(RtcEngineEventHandler(
      // 遇到错误
      onError: (ErrorCodeType err, String msg) 
        print('[onError] err: $err, msg: $msg');
      ,
      onJoinChannelSuccess: (RtcConnection connection, int elapsed) 
        // 加入频道成功
        setState(() 
          isJoined = true;
        );
      ,
      onUserJoined: (RtcConnection connection, int rUid, int elapsed) 
        // 有用户加入
        setState(() 
          remoteUid.add(rUid);
        );
      ,
      onUserOffline:
          (RtcConnection connection, int rUid, UserOfflineReasonType reason) 
        // 有用户离线
        setState(() 
          remoteUid.removeWhere((element) => element == rUid);
        );
      ,
      onLeaveChannel: (RtcConnection connection, RtcStats stats) 
        // 离开频道
        setState(() 
          isJoined = false;
          remoteUid.clear();
        );
      ,
    ));

    // 打开视频模块支持
    await _engine.enableVideo();
    // 配置视频编码器,编码视频的尺寸(像素),帧率
    await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
      ),
    );

    await _engine.startPreview();
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(),
      body: Stack(
        children: [
          FutureBuilder<bool?>(
              future: initStatus,
              builder: (context, snap) 
                if (snap.data != true) 
                  return Center(
                    child: new Text(
                      "初始化ing",
                      style: TextStyle(fontSize: 30),
                    ),
                  );
                
                return AgoraVideoView(
                  controller: VideoViewController(
                    rtcEngine: _engine,
                    canvas: const VideoCanvas(uid: 0),
                  ),
                );
              ),
          Align(
            alignment: Alignment.topLeft,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: List.of(remoteUid.map(
                      (e) => SizedBox(
                    width: 120,
                    height: 120,
                    child: AgoraVideoView(
                      controller: VideoViewController.remote(
                        rtcEngine: _engine,
                        canvas: VideoCanvas(uid: e),
                        connection: RtcConnection(channelId: channel),
                      ),
                    ),
                  ),
                )),
              ),
            ),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async 
          // 加入频道
          _engine.joinChannel(
            token: token,
            channelId: channel,
            uid: 0,
            options: ChannelMediaOptions(
              channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
              clientRoleType: ClientRoleType.clientRoleBroadcaster,
            ),
          );
        ,
      ),
    );
  

进阶调整

最后我们再来个进阶调整,前面 remoteUid 保存的只是远程用户 id ,如果我们将 remoteUid 修改为 remoteControllers 用于保存 VideoViewController ,那么就可以简单实现画面切换,比如「点击用户画面实现大小切换」这样的需求。

如下代码所示,简单调整后逻辑为:

  • remoteUid 从保存远程用户 id 变成了 remoteControllers 的 Map<int,VideoViewController>
  • 新增了currentController用于保存当前大画面下的 VideoViewController ,默认是用户自己
  • registerEventHandler 里将 uid 保存更改为 VideoViewController 的创建和保存
  • 在小窗处增加 InkWell 点击,在单击之后切换 VideoViewController 实现画面切换
class VideoChatPage extends StatefulWidget 
  const VideoChatPage(Key? key) : super(key: key);

  @override
  State<VideoChatPage> createState() => _VideoChatPageState();


class _VideoChatPageState extends State<VideoChatPage> 
  late final RtcEngine _engine;

  ///初始化状态
  late final Future<bool?> initStatus;

  ///当前 controller
  late VideoViewController currentController;

  ///是否加入聊天
  bool isJoined = false;

  /// 记录加入的用户id
  Map<int, VideoViewController> remoteControllers = ;

  @override
  void initState() 
    super.initState();
    initStatus = _requestPermissionIfNeed().then((value) async 
      await _initEngine();
      ///构建当前用户 currentController
      currentController = VideoViewController(
        rtcEngine: _engine,
        canvas: const VideoCanvas(uid: 0),
      );
      return true;
    ).whenComplete(() => setState(() ));
  

  Future<void> _requestPermissionIfNeed() async 
    await [Permission.microphone, Permission.camera].request();
  

  Future<void> _initEngine() async 
    //创建 RtcEngine
    _engine = createAgoraRtcEngine();
    // 初始化 RtcEngine
    await _engine.initialize(RtcEngineContext(
      appId: appId,
    ));

    _engine.registerEventHandler(RtcEngineEventHandler(
      // 遇到错误
      onError: (ErrorCodeType err, String msg) 
        print('[onError] err: $err, msg: $msg');
      ,
      onJoinChannelSuccess: (RtcConnection connection, int elapsed) 
        // 加入频道成功
        setState(() 
          isJoined = true;
        );
      ,
      onUserJoined: (RtcConnection connection, int rUid, int elapsed) 
        // 有用户加入
        setState(() 
          remoteControllers[rUid] = VideoViewController.remote(
            rtcEngine: _engine,
            canvas: VideoCanvas(uid: rUid),
            connection: RtcConnection(channelId: channel),
          );
        );
      ,
      onUserOffline:
          (RtcConnection connection, int rUid, UserOfflineReasonType reason) 
        // 有用户离线
        setState(() 
          remoteControllers.remove(rUid);
        );
      ,
      onLeaveChannel: (RtcConnection connection, RtcStats stats) 
        // 离开频道
        setState(() 
          isJoined = false;
          remoteControllers.clear();
        );
      ,
    ));

    // 打开视频模块支持
    await _engine.enableVideo();
    // 配置视频编码器,编码视频的尺寸(像素),帧率
    await _engine.setVideoEncoderConfiguration(
      const VideoEncoderConfiguration(
        dimensions: VideoDimensions(width: 640, height: 360),
        frameRate: 15,
      ),
    );

    await _engine.startPreview();
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(),
      body: Stack(
        children: [
          FutureBuilder<bool?>(
              future: initStatus,
              builder: (context, snap) 
                if (snap.data != true) 
                  return Center(
                    child: new Text(
                      "初始化ing",
                      style: TextStyle(fontSize: 30),
                    ),
                  );
                
                return AgoraVideoView(
                  controller: currentController,
                );
              ),
          Align(
            alignment: Alignment.topLeft,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                ///增加点击切换
                children: List.of(remoteControllers.entries.map(
                  (e) => InkWell(
                    onTap: () 
                      setState(() 
                        remoteControllers[e.key] = currentController;
                        currentController = e.value;
                      );
                    ,
                    child: SizedBox(
                      width: 120,
                      height: 120,
                      child: AgoraVideoView(
                        controller: e.value,
                      ),
                    ),
                  ),
                )),
              ),
            ),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async 
          // 加入频道
          _engine.joinChannel(
            token: token,
            channelId: channel,
            uid: 0,
            options: ChannelMediaOptions(
              channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
              clientRoleType: ClientRoleType.clientRoleBroadcaster,
            ),
          );
        ,
      ),
    );
  

完整代码如上图所示,运行后效果如下图所示,可以看到画面在点击之后可以完美切换,这里主要提供一个大体思路,如果有兴趣的可以自己优化并添加切换动画效果。

另外如果你想切换前后摄像头,可以通过 _engine.switchCamera(); 等 API 简单实现。

总结

从上面可以看到,其实跑完基础流程很简单,回顾一下前面的内容,总结下来就是:

  • 申请麦克风和摄像头权限
  • 创建和通过 App ID 初始化引擎
  • 注册 RtcEngineEventHandler 回调用于判断状态
  • 打开和配置视频编码支持,并且启动预览 startPreview
  • 调用 joinChannel 加入对应频道
  • 通过 AgoraVideoView 和 VideoViewController 配置显示本地和远程用户画面

当然,声网 SDK 在多人视频通话领域还拥有各类丰富的底层接口,例如虚拟背景、美颜、空间音效、音频混合等等,这些我们后面在进阶内容里讲到,更多 API 效果可以查阅 Flutter RTC API 获取

额外拓展

最后做个内容拓展,这部分和实际开发可能没有太大关系,纯粹是一些技术补充。

如果使用过 Flutter 开发过视频类相关项目的应该知道,Flutter 里可以使用外界纹理和PlatfromView两种方式实现画面接入,而由此对应的是 AgoraVideoView 在使用 VideoViewController 时,是有 useFlutterTexture 和 useAndroidSurfaceView 两个可选参数。

这里我们不讨论它们之间的优劣和差异,只是让大家可以更直观理解声网 SDK 在不同平台渲染时的差异,作为拓展知识点补充。

首先我们看 useFlutterTexture,从源码中我们可以看到:

  • 在 macOS 和 windows 版本中,声网 SDK 默认只支持 Texture 这种外界纹理的实现,这主要是因为 PC 端的一些 API 限制导致。
  • Android 上并不支持配置为 Texture ,只支持 PlatfromView 模式,这里应该是基于性能考虑。
  • 只有 iOS 支持 Texture 模式或者 PlatfromView 的渲染模式可选择,所以 useFlutterTexture 更多是针对 iOS 生效。

而针对 useAndroidSurfaceView 参数,从源码中可以看到,它目前只对 android 平台生效,但是如果你去看原生平台的 java 源码实现,可以看到其实不管是 AgoraTextureView 配置还是 AgoraSurfaceView 配置,最终 Android 平台上还是使用 TextureView 渲染,所以这个参数目前来看不会有实际的作用。

最后,就像前面说的 , 声网 SDK 是通过 Dart FFI 调用底层动态库进行支持,而这些调用目前看是通过AgoraRtcWrapper进行,比如通过 libAgoraRtcWrapper.so 再去调用 lib-rtc-sdk.so ,如果对于这一块感兴趣的,可以继续深入探索一下。

以上是关于实现基于声网Web SDK的视频会议的使用体验的主要内容,如果未能解决你的问题,请参考以下文章

视频直播系统解决方案—是基于声网SDK实现的

基于声网 Flutter SDK 实现多人视频通话

RTC月度小报5月 |教育aPaaS灵动课堂升级抢先体验VUE版 Agora Web SDK声网Agora与HTC达成合作

基于 Agora SDK 实现 iOS 端的多人视频互动

如何基于 Agora Android SDK 在应用中实现视频通话?

Unity实战篇 | 接入 声网SDK 实现 视频通话——自己动手做一个 视频通话