大数跨境
0
0

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

在 Flutter 多人视频中实现虚拟背景、美颜与空间音效 RTE开发者社区
2023-03-21
1
导读:Flutter 多人视频中一些特效功能的介绍

READING
前言

在之前的「基于声网 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,  ),);

属性

作用

lighteningContrastLevel

对比度,常与 lighteningLevel 搭配使用。取值越大,明暗对比程度越大

lighteningLevel

美白程度,取值范围为 [0.0,1.0],其中 0.0 表示原始亮度,默认值为 0.0。取值越大,美白程度越大

smoothnessLevel

磨皮程度,取值范围为 [0.0,1.0],其中 0.0 表示原始磨皮程度,默认值为 0.0。取值越大,磨皮程度越大

rednessLevel

红润度,取值范围为 [0.0,1.0],其中 0.0 表示原始红润度,默认值为 0.0。取值越大,红润程度越大

sharpnessLevel

锐化程度,取值范围为 [0.0,1.0],其中 0.0 表示原始锐度,默认值为 0.0。取值越大,锐化程度越大

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

没开美颜 开了美颜


03
色彩增强

 

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

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

  _engine.setColorEnhanceOptions(      enabled: true,      options: const ColorEnhanceOptions(          strengthLevel: 6.0, skinProtectLevel: 0.7));
如下图所示,因为摄像头采集到的视频画面可能存在色彩失真的情况,而色彩增强功能可以通过智能调节饱和度和对比度等视频特性,提升视频色彩丰富度和色彩还原度,最终使视频画面更生动。
开启增强之后画面更抢眼了。
没开增强
开了美颜+增强


属性

参数

strengthLevel

色彩增强程度。取值范围为 [0.0,1.0]。0.0 表示不对视频进行色彩增强。取值越大,色彩增强的程度越大。默认值为 0.5。

skinProtectLevel

肤色保护程度。取值范围为 [0.0,1.0]。0.0 表示不对肤色进行保护。取值越大,肤色保护的程度越大。默认值为 1.0。当色彩增强程度较大时,人像肤色会明显失真,你需要设置肤色保护程度;肤色保护程度较大时,色彩增强效果会略微降低。因此,为获取最佳的色彩增强效果,建议动态调节 strengthLevel 和 skinProtectLevel 以实现最合适的效果。

04
空间音效

 

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

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

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

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

属性

作用

speaker_azimuth

远端用户或媒体播放器相对于本地用户的水平角。取值范围为 [0,360],单位为度,例如 (默认)0 度,表示水平面的正前方;90 度,表示水平面的正左方;180 度,表示水平面的正后方;270 度,表示水平面的正右方;360 度,表示水平面的正前方;

speaker_elevation

远端用户或媒体播放器相对于本地用户的俯仰角。取值范围为 [-90,90],单位为度。(默认)0 度,表示水平面无旋转;-90 度,表示水平面向下旋转 90 度;90 度,表示水平面向上旋转 90 度

speaker_distance

远端用户或媒体播放器相对于本地用户的距离,取值范围为 [1,50],单位为米,默认值为 1 米。

speaker_orientation

远端用户或媒体播放器相对于本地用户的朝向。取值范围为 [0,180],单位为度。默认)0 度,表示声源和听者朝向同一方向;180: 180 度,表示声源和听者面对面

enable_blur

是否开启声音模糊处理

enable_air_absorb

是否开启空气衰减,即模拟声音在空气中传播的音色衰减效果:在一定的传输距离下,高频声音衰减速度快、低频声音衰减速度慢。

speaker_attenuation

远端用户或媒体播放器的声音衰减系数,取值范围为[0,1]。0:广播模式,即音量和音色均不随距离衰减;(0,0.5):弱衰减模式,即音量和音色在传播过程中仅发生微弱衰减;0.5:(默认)模拟音量在真实环境下的衰减,效果等同于不设置 speaker_attenuation 参数;(0.5,1]:强衰减模式,即音量和音色在传播过程中发生迅速衰减

enable_doppler

是否开启多普勒音效:当声源与接收声源者之间产生相对位移时,接收方听到的音调会发生变化

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

 

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

_engine.setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);

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

参数

作用

audioEffectOff

原声

roomAcousticsKtv

KTV

roomAcousticsVocalConcert

演唱会

roomAcousticsStudio

录音棚

roomAcousticsPhonograph

留声机

roomAcousticsVirtualStereo

虚拟立体声

roomAcousticsSpacial

空旷

roomAcousticsEthereal

空灵

roomAcousticsVirtualSurroundSound

虚拟环绕声

roomAcoustics3dVoice

3D 人声

voiceChangerEffectUncle

大叔

voiceChangerEffectOldman

老年男性

voiceChangerEffectBoy

男孩

voiceChangerEffectSister

少女

voiceChangerEffectGirl

女孩

voiceChangerEffectPigking

猪八戒

voiceChangerEffectHulk

绿巨人

styleTransformationRnb

R&B

styleTransformationPopular

流行

pitchCorrection

电音

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
最后

 

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

【声明】内容源于网络
0
0
RTE开发者社区
RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
内容 1122
粉丝 0
RTE开发者社区 RTE 开发者社区是聚焦实时互动领域的中立开发者社区。不止于纯粹的技术交流,我们相信开发者具备更加丰盈的个体价值。行业发展变革、开发者职涯发展、技术创业创新资源,我们将陪跑开发者,共享、共建、共成长。
总阅读653
粉丝0
内容1.1k