在之前的「根据声网 Flutter SDK 完成多人视频通话」里,咱们经过 Flutter + 声网 SDK 完美完成了跨平台和多人视频通话的作用,那么本篇咱们将在之前例子的基础上进阶介绍一些常用的特效功用。

本篇首要带你了解 SDK 里几个实用的 API 完成,相对简略

虚拟布景

虚拟布景是视频会议里最常见的特效之一,在声网 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(绿幕算法)两种不同场景下的抠图算法。

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

这儿需求留意的是,在官方的提示里,主张只在搭载如下芯片的设备上运用该功用(应该是关于 GPU 有要求):

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

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

美颜

美颜作为视频会议里另外一个最常用的功用,声网也供给了 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 参数后的美颜全体画面愈加白净,一起唇色也愈加显着。

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

颜色增强

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

如下代码所示,颜色增强 API 很简略,首要是调整 ColorEnhanceOptionsstrengthLevelskinProtectLevel 参数,也便是调整颜色强度和肤色维护的作用

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

如下图所示,由于摄像头收集到的视频画面可能存在颜色失真的状况,而颜色增强功用能够经过智能调节饱和度和比照度等视频特性,提升视频颜色丰厚度和颜色还原度,最终使视频画面更生动。

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

没开添加 开了美颜+增强
在 Flutter 多人视频中实现虚拟背景、美颜与空间音效
在 Flutter 多人视频中实现虚拟背景、美颜与空间音效
特点 参数
strengthLevel 颜色增强程度。取值规模为 [0.0,1.0]。0.0 表明不对视频进行颜色增强。取值越大,颜色增强的程度越大。默许值为 0.5。
skinProtectLevel 肤色维护程度。取值规模为 [0.0,1.0]。0.0 表明不对肤色进行维护。取值越大,肤色维护的程度越大。默许值为 1.0。 当颜色增强程度较大时,人像肤色会显着失真,你需求设置肤色维护程度; 肤色维护程度较大时,颜色增强作用会稍微降低。 因此,为获取最佳的颜色增强作用,主张动态调节 strengthLevel 和 skinProtectLevel 以完成最合适的作用。

空间音效

其实声响调教才是重头戏,声网既然叫声网,在音频处理上肯定不能落后,在声网 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 是否开启多普勒音效:当声源与接收声源者之间发生相对位移时,接收方听到的音调会发生变化

音频类的作用这儿就无法展现了,强烈推荐我们自己着手去试试。

人声响效

另外一个推荐的 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(() {});
  }
}

最终

本篇的内容作为上一篇的补充,相对来说内容仍是比较简略,不过能够看到不管是在画面处理仍是在声响处理上,声网 SDK 都供给了十分便捷的 API 完成,特别在声响处理上,由于文章限制这儿只展现了简略的 API 介绍,所以强烈主张我们自己尝试下这些音频 API ,真的十分风趣。