在 Flutter 多人视频通话中实现虚拟背景、美颜与空间音效
前言
在之前的「基于声网 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.8和path: ^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 很简单,主要是调整ColorEnhanceOptions的 strengthLevel和skinProtectLevel参数,也就是调整色彩强度和肤色保护的效果。
_engine.setColorEnhanceOptions(enabled: true,options: const ColorEnhanceOptions(strengthLevel: 6.0, skinProtectLevel: 0.7));
如下图所示,因为摄像头采集到的视频画面可能存在色彩失真的情况,而色彩增强功能可以通过智能调节饱和度和对比度等视频特性,提升视频色彩丰富度和色彩还原度,最终使视频画面更生动。
开启增强之后画面更抢眼了。
| 没开增强 | 开了美颜+增强 |
|---|---|
![]() | ![]() |

04 空间音效
其实声音调教才是重头戏,声网既然叫声网,在音频处理上肯定不能落后,在声网 SDK 里就可以通过enableSpatialAudio打开空间音效的效果。(点击查看空间音效接口文档)
_engine.enableSpatialAudio(true);
什么是空间音效?简单说就是特殊的 3D 音效,它可以将音源虚拟成从三维空间特定位置发出,包括听者水平面的前后左右,以及垂直方向的上方或下方。
本质上空间音效就是通过一些声学相关算法计算,模拟实现类似空间 3D 效果的音效实现。
同时你还可以通过setRemoteUserSpatialAudioParams来配置空间音效的相关参数,如下表格所示,可以看到声网提供了非常丰富的参数来让我们可以自主调整空间音效,例如这里面的enable_blur和enable_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);@overrideState<VideoChatPage> createState() => _VideoChatPageState();
}class _VideoChatPageState extends State<VideoChatPage> {late final RtcEngine _engine;///初始化状态late final Future<bool?> initStatus;///当前 controllerlate VideoViewController currentController;///是否加入聊天bool isJoined = false;/// 记录加入的用户idMap<int, VideoViewController> remoteControllers = {};@overridevoid initState() {super.initState();initStatus = _requestPermissionIfNeed().then((value) async {await _initEngine();///构建当前用户 currentControllercurrentController = 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();// 初始化 RtcEngineawait _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();}@overridevoid dispose() {_engine.leaveChannel();super.dispose();}@overrideWidget 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 分钟使用额度。如在开发过程中遇到疑问,可在声网开发者社区与官方工程师交流。
相关文章:
在 Flutter 多人视频通话中实现虚拟背景、美颜与空间音效
前言 在之前的「基于声网 Flutter SDK 实现多人视频通话」里,我们通过 Flutter 声网 SDK 完美实现了跨平台和多人视频通话的效果,那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能,包括虚拟背景、色彩增强、空间音频、基础变声…...
Ambari-web 架构
Ambari-web 使用的前端 Embar.js MVC 框架实现,Embar.js 是一个 TodoMVC 框架,涵盖了单页面应用(single page application)几乎所有的行为 Nodejs 是一个基于 Chrome JavaScript 运行时建立的一个平台,用来方便的搭建…...
对接百思买Best Buy EDI 的注意事项
在此前的文章:《Best Buy Drop Ship(Commerce hub) EDI业务测试常见报错及解决》中,我们介绍了在业务测试过程中遇到的常见报错及解决方案,以下在此基础上进行补充。 数据未能成功发送给Best Buy可能遇到的情况 Best Buy EDI项目传输业务报…...
2023年郑州重点建设项目名单公布,中创“算力数据中心”项目入选!
4月7日,郑州市人民政府网站公布2023年郑州市重点建设项目名单,名单共列项目680个,总投资1.08万亿元,年度计划投资2691亿元。 在创新驱动能力提升项目名单里,中创算力与人民网人民数据(国家大数据灾备中心&a…...
Pytorch 容器 - 1. Module类介绍
目录 1. 基于Module构建自己的网络 2. Module的初始化变量 3. Modules中需要子类 forward() 4. Modules中其他内置函数 1. 基于Module构建自己的网络 torch.nn.Module是所有神经网络模块的基类,如何定义自已的网络: 由于 Module 是神经网络模块的基…...
百度墨卡托坐标转化笔记
一、墨卡托坐标转化 调研了python和java多种实现方式的转换,发现有的不符合需求,原因还没找到。 我是用百度地图返回的poi边界(返回的是墨卡托坐标) 转换的原理没有深入研究,直接拿来用的,测试可行&…...
每日学术速递4.12
CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.HC 随着新的“生成代理”论文的发布,LLM刚刚达到了一个重要的里程碑——通过使用 LLM,生成代理能够在受《模拟人生》启发的交互式沙箱中模拟类人行为。代理架构扩展…...
HarmonyOS/OpenHarmony公司级技术开发团队硬件基本配置清单
有朋友公司咨询进入HarmonyOS/OpenHarmony领域,组建技术团队,硬件设备的基本配置应该是怎么样的比较合适?这个是进入鸿蒙开发领域相关配置的第一步,我们以一个基本的团队配置为例说明,供想进入的团队参考。 HarmonyOS/…...
新一代信息技术赋能,安科瑞搭建智慧水务体系的新思路
随着新时期治水方针的逐步落实,水利现代化、智能化建设已开启,物联网、图像识别、数字孪生等新技术的成熟,也为智慧水务体系的搭建提供了技术保障,新时代治水新思路正逐步得到落实。本文对智慧水务的总体架构与包含的建设内容进行…...
37岁测试工程师被裁,120天没找到工作,无奈...
从短期来看,程序员的确算是个不错的工作,薪水也比一般岗位高很多,但是从长远来看,程序员的中年危机会比其他岗位来的更早,很多程序员只有到了35岁左右,才能真正认清楚互联网行业,尤其是被裁之后…...
Java容器使用注意点
前置:问题 判空集合转map集合遍历集合去重集合转数组数组转集合 一:集合判空 《阿里巴巴 Java 开发手册》的描述如下: 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()0 的方式。 我们在开发中也…...
密文题解(图论+字典树)
题目大意 有一段长度为nnn的密文,密文的每一位都可以用一个非负整数来描述,并且每一位都有一个权值aia_iai。你可以操作任意多次,每次操作可以选择任意一段密文,花费选择的所有位上权值的异或和的代价获得这段密文每一位的异或…...
Baumer工业相机堡盟工业相机如何通过BGAPISDK里的工具函数来计算工业相机的实时帧率(C#)
Baumer工业相机堡盟工业相机如何通过BGAPISDK里函数来计算相机的实时帧率(C#)Baumer工业相机Baumer工业相机的帧率的技术背景Baumer工业相机的帧率计算方式在BufferEvent声明显示FrameID设计显示帧率的函数Baumer工业相机通过BGAPI SDK计算帧率的优势B…...
数据结构与常量(Java)
目录 1.字面常量 2. 数据类型 3. 变量 3.1 变量概念 3.2 语法格式 补充:变量 int long short double和float char boolean byte 4.类型转换 类型提升小结 5. 字符串类型 1. int 转成 String 2. String 转成 int 1.字面常量 类似System.Out.p…...
【LeetCode】剑指 Offer 54. 二叉搜索树的第k大节点 p269 -- Java Version
题目链接:https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/ 1. 题目介绍( 54. 二叉搜索树的第k大节点) 给定一棵二叉搜索树,请找出其中第 k 大的节点的值。 【测试用例】: 示例 1: 示例2&…...
[工具类] post请求 获取request对象, 获取request的请求体(body)参数
目录 引言: 1. 获取request对象的几种常用方式 -> 1.1 获取请求对象 通过请求上下文对象 获取信息[推荐] -> 1.2 在controller层直接获取[不推荐 侵害性太强] -> 1.3 interceptor中获取[部分业务中使用] -> 1.4 request常用api简介 2. 获取request的body的工具…...
Golang 多版本安装小工具G
voidint制作的Golang版本安装管理,非常好用。想装就装,想换版本就换版本 除了一些使用go install的场景可能有不兼容,主要是安装了工具有时候不能直接用。 GitHub - voidint/g: Golang Version Manager 使用方式很简单&a…...
day29—选择题
文章目录1.HashSet子类依靠什么方法区分重复元素(C)2.以下代码在编译和运行过程中会出现什么情况(A)3.有这么一段程序,执行的结果是(C)1.HashSet子类依靠什么方法区分重复元素(C&…...
day8 互斥锁/读写锁的概念及使用、死锁的避免
目录 互斥锁的概念和使用 线程通信 - 互斥 互斥锁的创建和销毁 互斥锁的创建 互斥锁的销毁 互斥锁的使用 申请锁 释放锁 互斥锁的概念和使用 线程通信 - 互斥 临界资源: 一次只允许一个任务(进程、线程)访问的共享资源;…...
2023-04-13 monetdb-str类型变长存储-分析
摘要: monetdb的列的基本抽象是BAT,但是对于列数据的存储方式, 对于固定长度和不固定长度,使用了不同的存储方式。 固定长度的数据比如int,int64之类的, 直接存储在了数据tail文件。 但是对于不固定长度比如string, 则使用另外一个独立的theap文件存储, tail文件仅保留对于…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...
基于Java Swing的电子通讯录设计与实现:附系统托盘功能代码详解
JAVASQL电子通讯录带系统托盘 一、系统概述 本电子通讯录系统采用Java Swing开发桌面应用,结合SQLite数据库实现联系人管理功能,并集成系统托盘功能提升用户体验。系统支持联系人的增删改查、分组管理、搜索过滤等功能,同时可以最小化到系统…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...




