在 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文件仅保留对于…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...




