在 Flutter 多人视频通话中实现虚拟背景、美颜与空间音效

news2024/11/18 20:31:33

前言

在之前的「基于声网 Flutter SDK 实现多人视频通话」里,我们通过 Flutter + 声网 SDK 完美实现了跨平台和多人视频通话的效果,那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能,包括虚拟背景、色彩增强、空间音频、基础变声功能。

本篇主要带你了解 SDK 里几个实用的 API 实现,相对简单。

01 虚拟背景

虚拟背景是视频会议里最常见的特效之一,在声网 SDK 里可以通过enableVirtualBackground方法启动虚拟背景支持(点击这里查看虚拟背景接口文档)。

首先,因为我们是在 Flutter 里使用,所以我们可以在 Flutter 里放一张assets/bg.jpg图片作为背景,这里有两个需要注意的点:

  • assets/bg.jpg图片需要在pubspec.yaml文件下的assets添加引用
  assets:
    - assets/bg.jpg
  • 需要在pubspec.yaml文件下添加path_provider: ^2.0.8path: ^1.8.2依赖,因为我们需要把图片保存在 App 本地路径下

如下代码所示,首先我们通过 Flutter 内的rootBundle读取到bg.jpg,然后将其转化为bytes, 之后调用getApplicationDocumentsDirectory获取路径,保存在的应用的/data"目录下,然后就可以把图片路径配置给enableVirtualBackground方法的source,从而加载虚拟背景。

Future<void> _enableVirtualBackground() async {
  ByteData data = await rootBundle.load("assets/bg.jpg");
  List<int> bytes =
      data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
  Directory appDocDir = await getApplicationDocumentsDirectory();
  String p = path.join(appDocDir.path, 'bg.jpg');
  final file = File(p);
  if (!(await file.exists())) {
    await file.create();
    await file.writeAsBytes(bytes);
  }

  await _engine.enableVirtualBackground(
      enabled: true,
      backgroundSource: VirtualBackgroundSource(
          backgroundSourceType: BackgroundSourceType.backgroundImg,
          source: p),
      segproperty:
          const SegmentationProperty(modelType: SegModelType.segModelAi));
  setState(() {});
}

如下图所示是都开启虚拟背景图片之后的运行效果,当然,这里还有两个需要注意的参数:

  • BackgroundSourceType :可以配置backgroundColor(虚拟背景颜色)、backgroundImg(虚拟背景图片)、backgroundBlur (虚拟背景模糊) 这三种情况,基本可以覆盖视频会议里的所有场景
  • SegModelType :可以配置为segModelAi(智能算法)或segModelGreen(绿幕算法)两种不同场景下的抠图算法。

这里需要注意的是,在官方的提示里,建议只在搭载如下芯片的设备上使用该功能(应该是对于 GPU 有要求):

  • 骁龙 700 系列 750G 及以上
  • 骁龙 800 系列 835 及以上
  • 天玑 700 系列 720 及以上
  • 麒麟 800 系列 810 及以上
  • 麒麟 900 系列 980 及以上

另外需要注意的是,为了将自定义背景图的分辨率与 SDK 的视频采集分辨率适配,声网 SDK 会在保证自定义背景图不变形的前提下,对自定义背景图进行缩放和裁剪。

02 美颜

美颜作为视频会议里另外一个最常用的功能,声网也提供了setBeautyEffectOptions方法支持一些基础美颜效果调整。(点击查看美颜接口文档)

如下代码所示,setBeautyEffectOptions方法里主要是通过BeautyOptions来调整画面的美颜风格,参数的具体作用如下表格所示。

这里的 .5 只是做了一个 Demo 效果,具体可以根据你的产品需求,配置出几种固定模版让用户选择。

_engine.setBeautyEffectOptions(
  enabled: true,
  options: const BeautyOptions(
    lighteningContrastLevel:
        LighteningContrastLevel.lighteningContrastHigh,
    lighteningLevel: .5,
    smoothnessLevel: .5,
    rednessLevel: .5,
    sharpnessLevel: .5,
  ),
);

运行后效果如下图所示,开了 0.5 参数后的美颜整体画面更加白皙,同时唇色也更加明显。

没开美颜开了美颜

03 色彩增强

接下来要介绍的一个 API 是色彩增强:setColorEnhanceOptions,如果是美颜还无法满足你的需求,那么色彩增强 API 可以提供更多参数来调整你的需要的画面风格。(点击查看色彩增强接口文档)

如下代码所示,色彩增强 API 很简单,主要是调整ColorEnhanceOptionsstrengthLevel和skinProtectLevel参数,也就是调整色彩强度和肤色保护的效果。

  _engine.setColorEnhanceOptions(
      enabled: true,
      options: const ColorEnhanceOptions(
          strengthLevel: 6.0, skinProtectLevel: 0.7));

如下图所示,因为摄像头采集到的视频画面可能存在色彩失真的情况,而色彩增强功能可以通过智能调节饱和度和对比度等视频特性,提升视频色彩丰富度和色彩还原度,最终使视频画面更生动。

开启增强之后画面更抢眼了。

没开增强开了美颜+增强

04 空间音效

其实声音调教才是重头戏,声网既然叫声网,在音频处理上肯定不能落后,在声网 SDK 里就可以通过enableSpatialAudio打开空间音效的效果。(点击查看空间音效接口文档)

_engine.enableSpatialAudio(true);

什么是空间音效?简单说就是特殊的 3D 音效,它可以将音源虚拟成从三维空间特定位置发出,包括听者水平面的前后左右,以及垂直方向的上方或下方。

本质上空间音效就是通过一些声学相关算法计算,模拟实现类似空间 3D 效果的音效实现。

同时你还可以通过setRemoteUserSpatialAudioParams来配置空间音效的相关参数,如下表格所示,可以看到声网提供了非常丰富的参数来让我们可以自主调整空间音效,例如这里面的enable_blurenable_air_absorb效果就很有意思,十分推荐大家去试试。

音频类的效果这里就无法展示了,强烈推荐大家自己动手去试试。

05 人声音效

另外一个推荐的 API 就是人声音效:setAudioEffectPreset, 调用该方法可以通过 SDK 预设的人声音效(点击查看人声音效接口文档),在不会改变原声的性别特征的前提下,修改用户的人声效果,例如:

_engine.setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);

声网 SDK 里预设了非常丰富的AudioEffectPreset,如下表格所示,从场景效果如 KTV、录音棚,到男女变声,再到恶搞的音效猪八戒等,可以说是相当惊艳。

PS:为获取更好的人声效果,需要在调用该方法前将setAudioProfile的 scenario 设为audioScenarioGameStreaming(3):

_engine.setAudioProfile(
  profile: AudioProfileType.audioProfileDefault,
  scenario: AudioScenarioType.audioScenarioGameStreaming);

当然,这里需要注意的是,这个方法只推荐用在对人声的处理上,不建议用于处理含音乐的音频数据。

最后,完整代码如下所示:

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 {
    if (Platform.isMacOS) {
      return;
    }
    await [Permission.microphone, Permission.camera].request();
  }

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

    _engine.registerEventHandler(RtcEngineEventHandler(
      // 遇到错误
      onError: (ErrorCodeType err, String msg) {
        if (kDebugMode) {
          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: const RtcConnection(channelId: cid),
          );
        });
      },
      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
  void dispose() {
    _engine.leaveChannel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Stack(
          children: [
            FutureBuilder<bool?>(
                future: initStatus,
                builder: (context, snap) {
                  if (snap.data != true) {
                    return const Center(
                      child: 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: cid,
              uid: 0,
              options: const ChannelMediaOptions(
                channelProfile:
                    ChannelProfileType.channelProfileLiveBroadcasting,
                clientRoleType: ClientRoleType.clientRoleBroadcaster,
              ),
            );
          },
        ),
        persistentFooterButtons: [
          ElevatedButton.icon(
              onPressed: () {
                _enableVirtualBackground();
              },
              icon: const Icon(Icons.accessibility_rounded),
              label: const Text("虚拟背景")),
          ElevatedButton.icon(
              onPressed: () {
                _engine.setBeautyEffectOptions(
                  enabled: true,
                  options: const BeautyOptions(
                    lighteningContrastLevel:
                        LighteningContrastLevel.lighteningContrastHigh,
                    lighteningLevel: .5,
                    smoothnessLevel: .5,
                    rednessLevel: .5,
                    sharpnessLevel: .5,
                  ),
                );
                //_engine.setRemoteUserSpatialAudioParams();
              },
              icon: const Icon(Icons.face),
              label: const Text("美颜")),
          ElevatedButton.icon(
              onPressed: () {
                _engine.setColorEnhanceOptions(
                    enabled: true,
                    options: const ColorEnhanceOptions(
                        strengthLevel: 6.0, skinProtectLevel: 0.7));
              },
              icon: const Icon(Icons.color_lens),
              label: const Text("增强色彩")),
          ElevatedButton.icon(
              onPressed: () {
                _engine.enableSpatialAudio(true);
              },
              icon: const Icon(Icons.surround_sound),
              label: const Text("空间音效")),
          ElevatedButton.icon(
              onPressed: () {                
                _engine.setAudioProfile(
                    profile: AudioProfileType.audioProfileDefault,
                    scenario: AudioScenarioType.audioScenarioGameStreaming);
                _engine
                    .setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);
              },
              icon: const Icon(Icons.surround_sound),
              label: const Text("人声音效")),
        ]);
  }

  Future<void> _enableVirtualBackground() async {
    ByteData data = await rootBundle.load("assets/bg.jpg");
    List<int> bytes =
        data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    Directory appDocDir = await getApplicationDocumentsDirectory();
    String p = path.join(appDocDir.path, 'bg.jpg');
    final file = File(p);
    if (!(await file.exists())) {
      await file.create();
      await file.writeAsBytes(bytes);
    }

    await _engine.enableVirtualBackground(
        enabled: true,
        backgroundSource: VirtualBackgroundSource(
            backgroundSourceType: BackgroundSourceType.backgroundImg,
            source: p),
        segproperty:
            const SegmentationProperty(modelType: SegModelType.segModelAi));
    setState(() {});
  }
}

06 最后

本篇的内容作为「基于声网 Flutter SDK 实现多人视频通话」的补充,相对来说内容还是比较简单,不过可以看到不管是在画面处理还是在声音处理上,声网 SDK 都提供了非常便捷的 API 实现,特别在声音处理上,因为文章限制这里只展示了简单的 API 介绍,所以强烈建议大家自己尝试下这些音频 API ,真的非常有趣。除此之外,还有许多场景与玩法,可以点击此处访问官网了解。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/419261.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HBase高手之路4-Shell操作

文章目录HBase高手之路3—HBase的shell操作一、hbase的shell命令汇总二、需求三、表的操作1&#xff0e;进入shell命令行2&#xff0e;创建表3&#xff0e;查看表的定义4&#xff0e;列出所有的表5&#xff0e;删除表1)禁用表2)启用表3)删除表四、数据的操作1&#xff0e;添加数…

TensorFlow 深度学习实战指南:1~5 全

原文&#xff1a;Hands-on Deep Learning with TensorFlow 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只关心如…

【通义千问】继ChatGPT爆火后,阿里云的大模型“通义千问”它终于来了

通义千问一、通义千问名字的由来二、通义千问和ChatGPT有什么区别呢&#xff1f;三、如何申请体验通义千问呢&#xff1f;四、未来通义千问能称为中国版的ChatGPT吗&#xff1f;五、通义千问什么时候正式发布呢&#xff1f;一、通义千问名字的由来 通义千问顾名思义&#xff0…

作物杂交——蓝桥杯20年省赛(JAVA)

题目链接&#xff1a; 用户登录https://www.lanqiao.cn/problems/506/learning/?page2&first_category_id1&sortstudents_count 题目描述 作物杂交是作物栽培中重要的一步。已知有 N 种作物 (编号 1 至 N )&#xff0c;第 i 种作物从播种到成熟的时间为 Ti​。作物…

少儿编程 电子学会图形化 scratch编程等级考试四级真题答案解析(判断题)2022年12月

2022年12月scratch编程等级考试四级真题 判断题(共10题,每题2分,共20分) 16、点击绿旗,反复按下空格键,可以使变量a的值在0和1之间反复变化 答案:对 考点分析:考查积木综合使用,重点考查变量积木的使用,按一下空格键,a变量值会改变5次,0-1-0-1-0-1,按第二下…

budibase <2.4.3 存在 ssrf 漏洞(CVE-2023-29010)

漏洞描述 budibase 是一个开源的低代码平台&#xff0c;元数据端点(metadata endpoint)是Budibase提供的一个REST API端点&#xff0c;用于访问应用程序的元数据信息。 budibase 2.4.3之前版本中存在 ssrf 漏洞&#xff0c;该漏洞可能影响 Budibase 自主托管的用户&#xff0…

安利安利-向大家推荐一个超级牛的etcd管理工具-EtcdKeeperFyne

etcd介绍 关于etcd的介绍大家可以看下这篇文章 etcd 开源仓库地址&#xff1a;EtcdKeeperFyne EtcdKeeperFyne 今天主要是向大家推荐一款使用起来特别方便的Etcd管理工具 EtcdKeeperFyne&#xff0c;具体运行起来的界面如下&#xff1a; 推荐原因 使用简单安装简单&…

卷积层输出尺寸计算 / 感受野尺寸计算

卷积层输出尺寸计算 输入图像a*a, 卷积核大小b*b, stride c, padding d 输出图像的尺寸&#xff1a;[(a - b 2d) // c] 1 (a - b 2d) 表示在输入图像两侧填充 d 个像素后&#xff0c;窗口在输入图像上最多能移动的距离&#xff0c;再加上 1 表示最后一个窗口的右侧边界…

博客文章效果

学习风宇blog md文档转html&#xff08;markdown-it的使用&#xff09;语法高亮、行号、一键复制toc生成目录sticky粘性定位 <style lang"scss"> import url(//at.alicdn.com/t/c/font_4004562_9v94jccafmc.css); import url(https://fonts.font.im/css?fam…

DFIG控制8: 不平衡电网下的网侧变换器控制

DFIG控制8&#xff1a; 不平衡电网下的网侧变换器控制。主要是添加网侧变换器的负序分量控制器。 本文基于教程的第8部分&#xff1a;DFIM Tutorial 8 - Asymmetrical Voltage Dips Analysis in DFIG based WT: Grid Side Converter Control 控制策略简介 来自&#xff1a;G…

过滤器(Filter)与拦截器(Interceptor)区别

1 过滤器&#xff08;Filter&#xff09; Servlet 中的过滤器 Filter 实现了 javax.servlet.Filter 接口的服务器端程序&#xff0c;主要用途是设置字符集&#xff08;CharacterEncodingFilter&#xff09;、控制权限、控制转向、用户是否已经登陆、有没有权限访问该页面等。 …

springboot配置跨域问题

近期自己搭建项目时&#xff0c;遇到一个跨域问题。我们以前项目解决跨域是在controller上加一个跨域注解CrossOrigin(allowCredentials "true")&#xff0c;很方便。但是在我自己搭建的项目中&#xff0c;启动时竟然报错了&#xff0c;错误如下&#xff1a; When …

图的传递闭包

给定一个有向图,对于给定图中的所有顶点对(i, j),找出一个顶点j是否可从另一个顶点i到达。这里的可达性是指从顶点i到j有一条路径。可达性矩阵称为图的传递闭包。 例如,考虑下面的图表 上述图的传递闭包为 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 该图以邻接矩阵的形式给出,…

抛弃 TCP 和 QUIC 的 HTTP

下班路上发了一则朋友圈&#xff1a; 周四听了斯坦福老教授 John Ousterhout 关于 Homa 的分享&#xff0c;基本重复了此前那篇 It’s Time To Rep… 的格调&#xff0c;花了一多半时间喷 TCP… Ousterhout 关于 Homa 和 TCP 之间的论争和论证&#xff0c;诸多反复回执&…

DAY15|102.二叉树的层序遍历。。。。等层序遍历的十道题

102.二叉树的层序遍历 代码随想录中的这题java和c不太一样 class Solution {public List<List<Integer>> resList new ArrayList<List<Integer>>();public List<List<Integer>> levelOrder(TreeNode root) {checkFun01(root,0);return …

NVIDIA- cuSPARSE(四)

cuSPARSE logging 日志记录机制&#xff0c; 可以通过在启动目标应用程序之前设置一下环境变量来启动cuSPARSE日志记录机制&#xff1a; CUSPARSE_LOG_LEVEL<level> level的取值&#xff1a; 0 Off 日志记录关闭1 Error只有报错会被记录2Trace启动CUDA内核的API调用将记…

网络应用程序设计(idea版)——实验四:会话管理

目录 实验预习报告 一、实验目的 二、实验原理 三、实验预习内容 实验报告 一、实验目的 二、实验要求 三、实验内容与步骤 实验预习报告 一、实验目的 1. 了解Web服务器对客户会话跟踪的各种方法&#xff1b; 2. 重点掌握使用HttpSession对象跟踪会话的方法&#…

OCAF——数据结构机制 Sample2

Email:dev_as@163.com Another example is the application for designing table lamps. The first label is allocated to the lamp unit. The tree definition of Lamp The root label cannot have brother labels. :[Root : (0)],根节点没有兄弟节点 Consequently, var…

ch5_4程序查询方式_程序中断方式_DMA方式

程序查询方式的流程 程序查询方式的接口电路 1. 程序查询方式 程序查询方式&#xff0c;需要通过cpu中的寄存器&#xff0c;完成数据从io设备到内存之间的传输。 1.1 程序查询方式流程 保存寄存器的内容&#xff1a; 如果寄存器中的数据是有用的&#xff0c;需要对寄存器中…

剑指offer:关于二叉树的汇总(c++)

1、重建二叉树&#xff1a; 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}&#xff0c;则重建二叉树并返回。 2、树的…