Flutter マルチプレーヤー ビデオで仮想背景、美しさ、空間音響効果を実現する

序文

前回の「Acoustics Flutter SDK に基づく複数のビデオ通話」で、 Flutter + Acoustics SDKによるクロスプラットフォームおよび複数人でのビデオ通話の効果を完全に実現したので、この記事では前の例に基づいて進めます。仮想背景、色強調、空間オーディオ、基本的なサウンド変更機能など、一般的に使用される特殊効果機能をいくつか紹介します。

この記事では主に、比較的単純な SDK でのいくつかの実用的な API 実装について説明します。

01 バーチャル背景

enableVirtualBackground仮想背景はビデオ会議で最も一般的な特殊効果の 1 つであり、仮想背景のサポートは Agora SDK のメソッドを通じて有効にすることができます。(ここをクリックして、仮想背景インターフェイスのドキュメントを表示します)。

まず、Flutter で使用しているため、assets/bg.jpg背景として Flutter に写真を配置できます。ここで、2 つの点に注意してください。

  • assets/bg.jpg画像は、pubspec.yamlファイルの下にassets参照を追加する必要があります
  assets:
    - assets/bg.jpg
  • アプリのローカルパスに画像を保存する必要があるため、ファイルをpubspec.yaml追加しpath_provider: ^2.0.8て依存する必要がありますpath: ^1.8.2

次のコードに示すように、まずFlutter でrootBundle読み取りbg.jpg、次に に変換しbytes、 を呼び出してgetApplicationDocumentsDirectoryパスを取得し、アプリケーションのディレクトリに保存してから、仮想をロードするメソッド/data"へのイメージ パスを構成します。バックグラウンド。enableVirtualBackgroundsource

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(() {});
}

下図のように仮想背景画像をオンにした後の操作効果ですが、もちろん注意が必要なパラメータが2つあります。

  • BackgroundSourceTypebackgroundColor:(仮想背景色)、backgroundImg(仮想背景画像)、 (仮想背景ぼかし)を設定できbackgroundBlur、この3つのシチュエーションでテレビ会議の基本的なシーンをカバーできます。
  • SegModelTypesegModelAi: 2 つの異なるシナリオ(スマート アルゴリズム) または(グリーン スクリーン アルゴリズム)segModelGreenでマッティング アルゴリズムとして構成できます。

ここで注意する必要があるのは、公式プロンプトでは、次のチップを搭載したデバイスでのみこの機能を使用することをお勧めすることです (GPU に必要なはずです)。

  • Snapdragon 700シリーズ 750G以上
  • Snapdragon 800 シリーズ 835 以上
  • Dimensity 700 シリーズ 720 以上
  • キリン800系810以上
  • キリン 900系 980以上

さらに、カスタム背景画像の解像度を SDK のビデオ キャプチャ解像度に適合させるために、SoundNet SDK はカスタム背景画像を変形せずにスケーリングおよびトリミングすることに注意してください。

02 ビューティー

美化は、ビデオ会議で最も一般的に使用されるもう 1 つの機能です。Shengwang は、setBeautyEffectOptionsいくつかの基本的な美化効果の調整をサポートする方法も提供します。(ビューティー インターフェース ドキュメントを表示するには、クリックしてください)。

以下のコードに示すように、setBeautyEffectOptionsメソッドは主にBeautyOptionsメソッドを通じて画面の美しさのスタイルを調整することであり、パラメータの具体的な機能は下の表に示されています。

The .5 here is just a demo effect. 具体的には、ユーザーが製品の要件に応じて選択できるように、いくつかの固定テンプレートを構成できます。

_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 は非常に単純で、主にColorEnhanceOptionslstrengthLeveskinProtectLevelパラメーターを調整します。つまり、色の強度と肌の色の保護の効果を調整します。

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

下の図に示すように、カメラでキャプチャされたビデオ画像には色の歪みがある可能性があるため、カラーエンハンスメント機能は、彩度やコントラストなどのビデオ特性をインテリジェントに調整して、ビデオの色の豊かさと色の再現を改善し、最終的にビデオ画像をより鮮やかに。

エンハンスメントをオンにした後、写真はより人目を引きます。

強化なし ビューティー + エンハンスメントをオンにする

04 空間効果音

実はサウンドチューニングがポイントなのですが、SoundNetはSoundNetと呼ばれているので、音声処理が遅れてはいけません.SoundNet SDKでは、enableSpatialAudio空間音響効果のエフェクトを開くことができます。(クリックして、空間オーディオ インターフェイスのドキュメントを表示します)

_engine.enableSpatialAudio(true);

空間音とは?簡単に言えば、リスナーの水平面、前後左右、垂直上下など、3 次元空間の特定の位置から音源が放出されるように仮想化できる特殊な 3D 音響効果です .

本質的に、空間音響効果は、空間 3D 効果と同様の音響効果の実現をシミュレートするために、いくつかの音響関連アルゴリズムによって計算されます。

同時に、下の表に示すように、空間音響効果の関連パラメーターを構成することもできます。サウンド ネットワークが非常に豊富なパラメーター セットを提供し、空間音響効果を個別に調整できることがわかります。たとえば、ここでの合計setRemoteUserSpatialAudioParams効果は非常に興味深く、強くお勧めします。enable_blurenable_air_absorb

オーディオ効果はここでは表示できません。実際に試してみることを強くお勧めします。

05 ボーカルエフェクト

もう 1 つの推奨される API は、人間の声の効果です。setAudioEffectPresetこのメソッドを呼び出して、SDK の事前設定された人間の声の効果を使用して、元の声の性別の特徴を変更せずにユーザーの声を変更します (クリックして、人間の声の効果インターフェイス ドキュメントを表示します)。

_engine.setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);

AudioEffectPresetSoundNet SDK には、下表のように、KTV、レコーディング スタジオなどのシーン効果から、男性と女性の声の変化、Zhu Bajie などのなりすまし効果音まで、さまざまなプリセットが用意されていますとても素晴らしいです。

PS: より良いボーカル効果を得るには、このメソッドを呼び出す前に、setAudioProfile を audioScenarioGameStreaming(3)scenarioに。

_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ラスト

この記事の内容は、「Acoustics Flutter SDK に基づく複数のビデオ コール」の補足です. 内容は比較的単純ですが、Acoustics SDK が特にサウンド処理において非常に便利な API 実装を提供していることがわかります.記事数が限られているため、ここでは簡単な API の紹介のみを示しますので、これらのオーディオ API を実際に試してみることを強くお勧めします。さらに、多くのシーンとゲームプレイがあります。詳細については、ここをクリックして公式 Web サイトにアクセスしてください。

開発者は、SoundNet SDK を試して、リアルタイムのオーディオおよびビデオ インタラクション シナリオを実現することもできます。Shengwang アカウントに登録して SDK をダウンロードすると、1 か月あたり 10,000 分の無料の使用量クォータを取得できます。開発プロセス中に質問がある場合は、Shengwang 開発者コミュニティの公式エンジニアと連絡を取ることができます。

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/agora/blog/8591407
おすすめ