Agora.ioを使用してFlutterアプリケーション用のスケーラブルなUIを構築する

この記事はAgora社の公式ブログからFlutterに関する記事を紹介します。
原文は[Building a Scalable UI for Your Flutter Application Using Agora](https://www.agora.io/en/blog/building-a-scalable-ui-for-your-flutter-application-using-agora/)に公開されています。

サンプルコードは[Github](https://github.com/Meherdeep/Agora-Flutter-Scalable-UI)も公開されています。


## はじめに
開発者が直面する最大の課題の1つは、拡張可能なアプリケーションを構築することです。ビデオ会議アプリケーションにおいて、参加者が多い場合、スケーリングの主な問題はローカルデバイスの帯域幅です。
また、参加者の数が増えると、アプリケーションが正しく動作する事がますます困難になります。

このチュートリアルでは、Agoraを使用して、着信ビデオストリームの帯域幅使用量を最適化することにより、最大17ユーザーまで拡張できるアプリケーションを構築する方法を説明します。


## 前提条件

 - Agora開発者アカウント
 - Flutter SDK
 - エディター
 - テスト用のAndroidまたはiOSデバイス


## プロジェクトの設定
1.プロジェクトディレクトリで、ターミナルウィンドウを開き、次のコマンドを実行してFlutterプロジェクトを作成します。

```
flutter create agora_scalable_ui
```

2.エディターで、新しく作成したプロジェクトを開きます。
3.pubspec.yamlにAgora Flutter SDKの最新バージョンを追加します。
4.AndroidManifest.xml ファイル（Android）または Info.plist ファイル（iOS）に関連する権限を追加します 。
Android：

```
<uses-permission android:name="android.permission.INTERNET"/> 
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
```
iOS:
NSCameraUsageDescriptionとNSMicrophoneUsageDescriptionを追加します。

## 実装
非常にシンプルなグリッドビューを使用します。今回の目標は、ユーザーが参加するときにこのアプリケーションを拡張することです。これを行うために、帯域幅の使用を最適化して、すべてのストリームが問題なく同時に機能するようにします。

### 入室前画面の作成
入室前画面では、ユーザーが参加したいチャンネルの名前を入力するように促し、カメラとマイクの許可を要求する責任があります。

```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _channelController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        physics: BouncingScrollPhysics(),
        child: Container(
          height: MediaQuery.of(context).size.height,
          width: double.infinity,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Text(
                'Agora Scalable UI',
                style: TextStyle(fontSize: 30),
              ),
              Container(
                width: MediaQuery.of(context).size.width * 0.7,
                child: TextFormField(
                  controller: _channelController,
                  decoration: InputDecoration(hintText: 'Channel Name'),
                ),
              ),
              Container(
                width: MediaQuery.of(context).size.width * 0.5,
                child: MaterialButton(
                  color: Colors.blue,
                  onPressed: () async {
                    await [Permission.camera, Permission.microphone].request();
                    Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) =>
                          VideoCallPage(channelName: _channelController.text),
                    ));
                  },
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        'Submit',
                        style: TextStyle(color: Colors.white),
                      ),
                      Icon(
                        Icons.arrow_right,
                        color: Colors.white,
                      )
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```
ユーザーのアクセス許可をリクエストするには、permission handlerを使用しています。

### ビデオ通話画面の作成
1.必要な変数をすべて準備します。

```dart
static final _users = <int>[];
final _infoStrings = <String>[];
bool muted = false;
late RtcEngine _engine;
```

2.AgoraSDKのメソッドのイニシャライズをします。

```dart
@override
void initState() {
  super.initState();
  // initialize agora sdk
  initialize();
}

Future<void> initialize() async {
  if (APP_ID.isEmpty) {
    setState(() {
      _infoStrings.add('APP_ID missing, please provide your APP_ID in settings.dart',);
      _infoStrings.add('Agora Engine is not starting');
    });
    return;
  }
  await _initAgoraRtcEngine();
  _addAgoraEventHandlers();
  await _engine.joinChannel(null, widget.channelName, null, 0);
  await _engine.enableDualStreamMode(true);
  await _engine.setParameters("""
         { "che.video.lowBitRateStreamParameter": {
           "width":160,"height":120,"frameRate":5,"bitRate":45
         }}
       """);
}
```
上記のコードスニペットでは、デュアルストリームモードを有効にしてAgoraエンジンをセットアップしました。デュアルストリームは、クライアントが2つのストリームを同時に公開できるようにするAgoraの機能です。1つのストリームは高解像度とビットレート用で、もう1つのストリームは低解像度とビットレート用です。このデュアルストリーム設定では、リモートクライアントがストリームにサブスクライブすると、帯域幅要件に基づいてより低いストリームに切り替えることができます。

3.AgoraSDKのイニシャライズをします。

```dart
Future<void> _initAgoraRtcEngine() async {
  _engine = await RtcEngine.create(APP_ID);
  await _engine.enableVideo();
}
```
この_initAgoraRtcEngine関数は、AgoraSDKで提供されるすべてのメソッドを参照するために使用するAgoraRtcEngineのオブジェクトを作成します。

4.AgoraSDKのイベントハンドラーを追加します。

```dart
void _addAgoraEventHandlers() {
  _engine.setEventHandler(RtcEngineEventHandler(
    joinChannelSuccess: (channel, uid, elapsed) {
      setState(() {
        final info = 'onJoinChannel: $channel, uid: $uid';
        _infoStrings.add(info);
      });
    },
    leaveChannel: (stats) {
      setState(() {
        _infoStrings.add('onLeaveChannel');
        _users.clear();
      });
    },
    userJoined: (uid, elapsed) {
      setState(() {
        final info = 'userJoined: $uid';
        _infoStrings.add(info);
        _users.add(uid);
      });
      if (_users.length >= 5) {
        print("Fallback to Low quality video stream");
        _engine.setRemoteDefaultVideoStreamType(VideoStreamType.Low);
      }
    },
    userOffline: (uid, reason) {
      setState(() {
        final info = 'userOffline: $uid , reason: $reason';
        _infoStrings.add(info);
        _users.remove(uid);
      });
      if (_users.length <= 3) {
        print("Go back to High quality video stream");
        _engine.setRemoteDefaultVideoStreamType(VideoStreamType.High);
      }
    },
  ));
}
```

userJoinedコールバックでは、次のことを行っています。

 - _usersのリストにリモートビデオのUIDを追加する。
 - リモートユーザーの数が5人に増えたかどうかを確認して、デュアルストリームモードで低画質ストリームに切り替えるかどうかを判断します。

userOfflineコールバックでは、我々は次のことを行っています。

 - _usersリストからUIDを削除する。
 - リモートユーザーの数が3人に減った場合は、デュアルストリームモードで高画質ストリームに切り替えます。

5.ローカルおよびリモートユーザービデオのグリッドビューを作成します。

```dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Agora Group Video Calling'),
    ),
    backgroundColor: Colors.black,
    body: Center(
      child: Stack(
        children: <Widget>[
          _viewRows(),
        ],
      ),
    ),
  );
}

/// Helper function to get list of native views
List<Widget> _getRenderViews() {
  final List<StatefulWidget> list = [];
  list.add(rtc_local_view.SurfaceView());
  _users.forEach(
    (int uid) => list.add(
      rtc_remote_view.SurfaceView(
        uid: uid,
      ),
    ),
  );
  return list;
}

/// Video view wrapper
Widget _videoView(view) {
  return Expanded(child: Container(child: view));
}

/// Video view row wrapper
Widget _expandedVideoRow(List<Widget> views) {
  final wrappedViews = views.map<Widget>(_videoView).toList();
  return Expanded(
    child: Row(
      children: wrappedViews,
    ),
  );
}

/// Video layout wrapper
Widget _viewRows() {
  final views = _getRenderViews();
  if (views.length == 1) {
    return Container(
      child: Column(
        children: <Widget>[_videoView(views[0])],
      ),
    );
  } else if (views.length == 2) {
    return Container(
        child: Column(
          children: <Widget>[
          _expandedVideoRow([views[0]]),
          _expandedVideoRow([views[1]])
        ],
      ));
  } else if (views.length > 2 && views.length % 2 == 0) {
    return Container(
      child: Column(
        children: [
          for (int i = 0; i < views.length; i = i + 2)
            _expandedVideoRow(
            views.sublist(i, i + 2),
          ),
        ],
      ),
    );
  } else if (views.length > 2 && views.length % 2 != 0) {
    return Container(
      child: Column(
        children: <Widget>[
          for (int i = 0; i < views.length; i = i + 2)
            i == (views.length - 1)
                ? _expandedVideoRow(views.sublist(i, i + 1))
                : _expandedVideoRow(views.sublist(i, i + 2)),
        ],
      ),
    );
  }
  return Container();
}
```
ここでは、_usersリストに存在するUIDを渡すだけで、ビューのリストが生成されます。これらのビューは、ユーザーが参加すると自動的に拡大縮小するグリッドを生成するために使用されます。

### クリーンアップ処理
最後に、disposeメソッドを作成します。ここで、AgoraSDKに関連するすべてのリソースをクリーンアップします。

```dart
@override
void dispose() {
  // clear users
  _users.clear();
  // destroy sdk
  _engine.leaveChannel();
  _engine.destroy();
  super.dispose();
}
```

## 実行結果
これで、受信ストリームの設定を最適化することで最大17ユーザーまで拡張できるビデオチャットアプリケーションができました。

![building-a-scalable-ui-for-your-flutter-application-using-agora-1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/4f51e95a-2123-87da-3b07-bff6fcc67f01.png)


## 最後に                                                                                                    
[**agora.ioに関するお問い合わせはこちらから**](https://jp.vcube.com/lp/agora?utm_campaign=Agora&utm_source=Qiita&arc_id=195a38561c4e5bf340ad)   
[<img width="912" alt="スクリーンショット 0001-08-15 13.41.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/5d815376-9b89-08fa-9434-6fa7e8e67bcf.png">](https://jp.vcube.com/lp/agora?utm_campaign=Agora&utm_source=Qiita&arc_id=195a38561c4e5bf340ad)