FlutterでWebRTCをやってみる with AgoraSDK

# はじめに
`WebRTC` を使ったアプリを開発する機会があり、何かお手軽な `package` ないかなーと探していたところ `Flutter` で `WebRTC` を手軽に利用できる `plugin` を見つけたので試しにアプリを作成してみました。


# WebRTC とは
`WebRTC` (Web Real-Time Communication) とは、ビデオや音声、データをブラウザ間でやり取り可能にするための規格で、`Google` によってオープンソース化されました。 ユーザーはその `API` を経由することでリアルタイム通信を実現できます。

[参考: WebRTCの基本とP2P通信が成立するまでを学ぶ](https://qiita.com/daitasu/items/ae21b16361eb9f65ed43)

最近ではコロナの影響もあり、ウェブ会議システムやチャットツールなどの利用者が急増しています。

- Zoom
- Hang out
- Discord
- Microsoft Teams

<img width="1141" alt="スクリーンショット 2020-05-25 04.37.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/28b6a6d1-953c-4e48-03d8-8cfdb7ba26fe.png">


コロナをきっかけに一般に広く知られるようになり、今まで利用するに至らなかった勢が利用していました（周りでも）。
今後これらのツールが一般的に利用されるようになるのではないでしょうか。


# Agora.IO SDK

<p align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/b6b63df7-2d30-6677-0735-3a9bd465e7e7.png" width="400" style="display:flex;">
</p>

`Agora.IO` が開発している、ビデオ通話やライブ配信を構築できる `SDK` です。
 基盤となるこの `SDK` を利用して様々な言語やプラットフォームで利用することができます。
日本では `NTT Communications` の `SkyWay` に相当するものです。

`Flutter` では [agora_rtc_engine | Flutter package](https://pub.dev/packages/agora_rtc_engine) を利用します。


# 実装
実際にサンプルアプリを実装していきます.

## `Agora Project` の作成

`Agora` を利用するには `Agora.IO` に `Project` を作成し、 `AppID` を入手する必要があります．
無料枠が `10000 minutes` 程あるので当分は無料で利用できますし、無料枠を超えても金額を請求されることはないので安心してください．

まずは[こちら](https://sso.agora.io/en/v2/signup)から登録して `Project` を作成します．
[Agora | Sign Up](https://sso.agora.io/en/v2/signup)

`Project` を作成後、`Project Management` をクリックして作成した `Project` の `AppID` をメモしておきます．

<img width="1164" alt="スクリーンショット 2020-05-21 18.21.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/f0cb314e-1bcf-4a9e-8c7a-ddbbab3c7643.png">


## Platform settings
`Platform` 毎に権限周りや固有の設定をしていきます.

### `iOS`

`ios/Runner/info.plist` にカメラとマイクの権限を追加

```xml:info.plist
<key>NSCameraUsageDescription</key>
<string>Use camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Use mic</string>
<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>
```

`WebView` を利用するので以下も追加

```xml:info.plist
<key>io.flutter.embedded_views_preview</key>
<true/>
```

### `Android`

`android/app/src/main/AndroidManifest.xml` に以下の権限を追加します.

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```

`android/app/src/proguard-rules.pro` を作成し、以下を書いておきます 
(難読化によるアプリクラッシュを防ぐ).

```dart:proguard-rules.pro
-keep class io.agora.**{*;}
```

`platform` 固有の設定は以上です.

## `agora_rtc_engine` 周りの実装 

ここがメインの部分になります.

### `agora_rtc_engine` の追加

```yaml:pubspeck.yaml
dependencies:
  agora_rtc_engine: ^1.0.12
```

## Controller

今回は `state_notifier` と `freezed` パッケージを利用して実装しました。
`agora_rtc_engine` に関する処理は全部この中でやっています。

```dart:WebRtcController.dart
import 'package:agora_example/models/entities/entities.dart';
import 'package:agora_example/utils/constants.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:state_notifier/state_notifier.dart';

import 'webrtc_state.dart';

class WebRtcController extends StateNotifier<WebRtcState> {
  WebRtcController({
    @required String roomName,
  }) : super(const WebRtcState()) {
    debugPrint('$tag: init()');
    initWebRtc(roomName: roomName);
  }

  static const tag = 'WebRtcController';

  Future<void> initWebRtc({@required String roomName}) async {
    /// initialize する前に必ず Permission Request を行う
    await Permission.camera.request();
    await Permission.microphone.request();

    /// 先程メモした AppID を利用
    await AgoraRtcEngine.create(Constants.appId);

    /// AV 周りに関する設定
    await AgoraRtcEngine.enableAudio();
    await AgoraRtcEngine.enableVideo();
    await AgoraRtcEngine.setChannelProfile(ChannelProfile.Communication);
    await AgoraRtcEngine.enableWebSdkInteroperability(true);

    /// Event Listener を設定する

    /// 自分が Join に成功した時
    AgoraRtcEngine.onJoinChannelSuccess = _onJoinChannelSuccess;

    /// 相手が Join に成功した時
    AgoraRtcEngine.onUserJoined = _onUserJoined;

    /// 自分が Leave した時
    AgoraRtcEngine.onLeaveChannel = _onLeaveChannel;

    /// 相手が Offline になった時
    AgoraRtcEngine.onUserOffline = _onUserOffline;

    /// Join する処理
    await AgoraRtcEngine.startPreview();
    await AgoraRtcEngine.joinChannel(null, roomName, null, 0);
  }

  Future<void> toggleLocalAudio() async {
    final localAvStatus = state.localAvStatus;
    await AgoraRtcEngine.muteLocalAudioStream(localAvStatus.mic);
    state = state.copyWith(
      localAvStatus: localAvStatus.copyWith(
        mic: !localAvStatus.mic,
      ),
    );
  }

  Future<void> toggleLocalVideo() async {
    final localAvStatus = state.localAvStatus;
    await AgoraRtcEngine.muteLocalVideoStream(localAvStatus.video);
    state = state.copyWith(
      localAvStatus: localAvStatus.copyWith(
        video: !localAvStatus.video,
      ),
    );
  }

  void switchView(int viewIndex) {
    state = state.copyWith(viewIndex: viewIndex);
  }

  void _onJoinChannelSuccess(String roomName, int uid, int elapsed) {
    debugPrint('$tag: onJoinChannelSuccess -> $uid');
    final users = [...state.users, WebRtcUser(uid: uid)];
    state = state.copyWith(
      users: users,
    );
  }

  void _onUserJoined(int uid, int elapsed) {
    debugPrint('$tag: onUserJoined -> $uid');
    final users = [...state.users, WebRtcUser(uid: uid)];
    state = state.copyWith(
      users: users,
    );
  }

  void _onLeaveChannel() {
    debugPrint('$tag: onLeaveChannel');
    state = state.copyWith(users: []);
  }

  void _onUserOffline(int uid, int reason) {
    debugPrint('$tag: onUserOffline -> $uid');
    final users = <WebRtcUser>[];
    for (final user in state.users) {
      if (user.uid != uid) {
        users.add(user);
      }
    }
    state = state.copyWith(users: users, viewIndex: 0);
  }

  @override
  void dispose() {
    super.dispose();
    /// agora_rtc_engine の破棄
    AgoraRtcEngine.leaveChannel();
    AgoraRtcEngine.stopPreview();
    AgoraRtcEngine.destroy();
  }
}


```


## View

`Agora` からは `PlatformView` が提供されるのでそれを利用します。

```dart:webrtc_view.dart
AgoraRenderWidget(
    uid, /// Join に成功した場合に取得できる uid
    local: true, /// 自分であれば true, それ以外は false
    preview: true,
    mode: VideoRenderMode.Hidden /// object-fit か object-cover か
)
```

## Page

```dart:webrtc_page.dart
import 'package:agora_example/pages/webrtc_page/room_user_list.dart';
import 'package:agora_example/pages/webrtc_page/webrtc_view.dart';
import 'package:agora_example/pages/webrtc_page/call_action_button.dart';
import 'package:agora_example/models/models.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';

class WebRtcPage extends StatelessWidget {
  const WebRtcPage._({Key key}) : super(key: key);

  static Widget wrapped({@required String roomName}) {
    return MultiProvider(
      providers: [
        StateNotifierProvider<WebRtcController, WebRtcState>(
          create: (context) => WebRtcController(roomName: roomName),
        ),
      ],
      child: const WebRtcPage._(),
    );
  }

  @override
  Widget build(BuildContext context) {
    final localAvStatus = context.select(
      (WebRtcState state) => state.localAvStatus,
    );
    return Scaffold(
      appBar: null,
      body: Stack(
        children: [
          Column(
            children: [
              SizedBox(
                height: MediaQuery.of(context).size.height / 2,
                child: const WebRtcView(),
              ),
              SizedBox(
                height: MediaQuery.of(context).size.height / 2,
                child: const RoomUserList(),
              ),
            ],
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 30),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  CallActionButton(
                    tag: 'mic mute',
                    icon: localAvStatus.mic ? Icons.mic : Icons.mic_off,
                    color: Colors.black,
                    backgroundColor: Colors.white,
                    onPressed: () {
                      context.read<WebRtcController>().toggleLocalAudio();
                    },
                  ),
                  CallActionButton(
                    tag: 'call end',
                    icon: Icons.call_end,
                    backgroundColor: Colors.red,
                    onPressed: Navigator.of(context).pop,
                  ),
                  CallActionButton(
                    tag: 'video mute',
                    icon: localAvStatus.video
                        ? Icons.videocam
                        : Icons.videocam_off,
                    color: Colors.black,
                    backgroundColor: Colors.white,
                    onPressed: () {
                      context.read<WebRtcController>().toggleLocalVideo();
                    },
                  ),
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

```
 
# Demo

予め `iPad` の方を起動しておき、同じ `Room` に `Join` しています。

<p align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/0d89f27d-dfa6-ea27-8a19-55f7e4e4a964.gif" width=300px/>
</p>

映像の遅延もなく、動作もサクサクで快適に動作しました。
`Flutter` で手軽に `WebRTC` を利用することができるのは嬉しいですね。

# 終わりに
今回作成したサンプルアプリは [GitHub](https://github.com/yukitaka13-1110/flutter_webrtc_agora_example.git) に公開しているのでご自由にお使いください。

[yukitaka13-1110 / flutter_webrtc_agora_example](https://github.com/yukitaka13-1110/flutter_webrtc_agora_example.git)


コロナウイルスの影響もあってリモートワークやその他遠隔コミュニケーションでビデオ通話ができるツールがの需要が高まってきているのを感じているので、その裏側を作るのも面白そうだなと思いました。