FlutterでWebRTCをやってみる with Twilio

# はじめに
以前、[Qiita | FlutterでWebRTCをやってみる with AgoraSDK](https://qiita.com/yukitaka13-1110/items/e7350a5bc50f2056253f) という記事を書いたのですが、その時点で Twilio の Flutter plugin はありませんでした。
最近、Twilio の Flutter plugin が Publish されていたので実際に触ってみることにしました。

# Twilio
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/21545891-4894-bd60-632b-5df5b03bac35.png)

Twilio 社が開発した Twilio は コミュニケーションに特化した API を提供しています。
提供する機能は以下のように幅広く、様々なシチュエーションで利用可能です。


![スクリーンショット 2020-12-10 1.18.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/60f1d558-345c-b536-9505-52e9d713fb43.png)

この中でも、ビデオ通話を利用したアプリを Flutter で実装してみます。

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

## Twilio に登録
Twilio を利用するには認証やその準備などがあるので、利用ハードルは少し高いです。
ちょっと試すだけなら全然大丈夫ですが、長く利用する予定がある場合は以下の記事のような手順を踏む必要があります。

[TwilioでBundles（本人認証）の設定を行う（個人編）](https://qiita.com/mobilebiz/items/579d1920a8b0bca96459)


## Token 用の function 作成
各種設定は以下の記事を参考にすると良いです。

[Qiita | Twilio WebRTCハンズオン 2020版](https://qiita.com/mobilebiz/items/bdace9699ce77dbc7279#video-api%E3%81%AE%E8%AA%8D%E8%A8%BC)

## Flutter Project の作成

```
flutter create -t app --org com.example.twilioSample --project-name flutter_twilio_sample -i swift -a kotlin ./
```

## 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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
```

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

```dart:proguard-rules.pro
-keep class tvi.webrtc.** { *; }
-keep class com.twilio.video.** { *; }
-keep class com.twilio.common.** { *; }
-keepattributes InnerClasses
```

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

## `twilio_programmable_video` を用いた実装


### Plugin の追加
以下の Plugin を追加してください。

[Dart pub | twilio_programmable_video](https://pub.dev/packages/twilio_programmable_video)

### Twilio 部分の実装

状態管理は `flutter_riverpod` + `state_notifier` を利用してみます。

- [Dart pub | flutter_riverpod](https://pub.dev/packages/flutter_riverpod)
- [Dart pub | state_notifier](https://pub.dev/packages/state_notifier)

#### State

```dart:webrtc_state.dart
part 'webrtc_state.freezed.dart';

@freezed
abstract class WebRtcState with _$WebRtcState {
  const factory WebRtcState({
    @Default(VideoInfo()) VideoInfo localVideoInfo,
    @Default(VideoInfo()) VideoInfo remoteVideoInfo,
  }) = _WebRtcState;
}
```

#### Notifier

```dart:webrtc_controller.dart
class WebRtcController extends StateNotifier<WebRtcState> {
  WebRtcController() : super(const WebRtcState()) {
    _initializeWebRtc();
  }

  CameraCapturer _cameraCapturer;
  Room _room;
  StreamSubscription _onConnectedSubscription;
  StreamSubscription _onParticipantConnectedSubscription;
  StreamSubscription _onParticipantDisconnectedSubscription;
  final _remoteParticipantSubscriptions = <StreamSubscription>[];

  Future<void> _initializeWebRtc() async {
    // Token の取得
    final response = await http.get(Constants.twilioTokenEndpoint);
    final data = json.decode(response.body) as Map<String, dynamic>;

    // Mode の設定
    await TwilioProgrammableVideo.debug(dart: true, native: true);

    // 権限のリクエスト
    await TwilioProgrammableVideo.requestPermissionForCameraAndMicrophone();

    // Video Source の取得
    _cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA);

    // 接続情報の定義
    final connectOptions = ConnectOptions(
      data['token'] as String,
      roomName: 'VideoRoom',
      preferredAudioCodecs: [OpusCodec()],
      audioTracks: [LocalAudioTrack(true)],
      dataTracks: [LocalDataTrack()],
      videoTracks: [LocalVideoTrack(true, _cameraCapturer)],
      enableNetworkQuality: true,
      networkQualityConfiguration: NetworkQualityConfiguration(
        remote: NetworkQualityVerbosity.NETWORK_QUALITY_VERBOSITY_MINIMAL,
      ),
      enableDominantSpeaker: true,
    );

    // Room に接続
    _room = await TwilioProgrammableVideo.connect(connectOptions);

    // イベントリスナーをセット
    if (_room != null) {
      _onConnectedSubscription = _room.onConnected.listen(_onConnected);
      _onParticipantConnectedSubscription =
          _room.onParticipantConnected.listen(_onParticipantConnected);
      _onParticipantDisconnectedSubscription =
          _room.onParticipantDisconnected.listen(_onParticipantDisconnected);
    }
  }
}
```

#### Provider

```dart:state_providers.dart
final webRtcController = StateNotifierProvider.autoDispose(
  (_) => WebRtcController(),
);
```

#### UI

```dart:webrtc_room_page.dart
class WebRtcRoomPage extends ConsumerWidget {
  const WebRtcRoomPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final controller = watch(webRtcController);
    final state = watch(webRtcController.state);

    return Scaffold(
      appBar: null,
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          _videoWidget(context, state.localVideoInfo.widget),
          _videoWidget(context, state.remoteVideoInfo.widget),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await controller.endCall();
          Navigator.of(context)?.pop();
        },
        backgroundColor: Colors.red,
        child: const Icon(
          Icons.call_end,
          color: Colors.white,
        ),
      ),
    );
  }

  Widget _videoWidget(BuildContext context, Widget widget) {
    final height = MediaQuery.of(context).size.height;
    final width = MediaQuery.of(context).size.width;

    if (widget == null) {
      return Placeholder(
        fallbackWidth: width,
        fallbackHeight: height / 2.0,
      );
    }

    return SizedBox(
      width: width,
      height: height / 2.0,
      child: widget,
    );
  }
}

```

# デモ
![IMB_fmPZBj.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/7c1adcb7-fcc9-c931-ad01-d4a20a7e51cd.gif)

特に難しいところもなく `Flutter` から `Twilio` を利用することができました。

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

[yukitaka13-1110 / flutter-twilio-programmable-video-sample](https://github.com/yukitaka13-1110/flutter-twilio-programmable-video-sample.git)


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