共计 8419 个字符,预计需要花费 22 分钟才能阅读完成。
前言
在之前的「基于声网 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
的 strengthLeve
l 和 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); | |
@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 分钟应用额度。如在开发过程中遇到疑难,可在声网开发者社区与官网工程师交换。