Expo Application Services(EAS)とAgoraを使って動画版Clubhouseを作った

#目次
[1.作ったもの](#1-作ったもの)
[2.Expo Application Services](#2-Expo Application Services)
[3.デモアプリを作ろう](#3-デモアプリを作ろう)
[4.補足](#4-補足)
[5.まとめ](#5-まとめ)

#1. 作ったもの

動画配信SNS、[Ocacatalk](https://ocacatalk.ml)を作りました。

![img.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/dc7dc47c-566a-a8d9-39e1-d05b983fccbb.jpeg)

Clubhouseの動画版です。テキストチャットもできます。Clubhouse使ったことないので伝え聞く情報から完全に想像だけで作りました。よかったら遊んでみてください。iPhone/Android両対応です。

#2. Expo Application Services

## Expo CLI

これまで私のアプリは全てExpo Managed Workflowで開発してきました。Expoとは簡単にいうとReact Nativeのアプリ開発を超便利にしてくれる開発ツールです。XcodeやAndroid Studioをまったく触らなくても開発からリリースまで出来てしまいます。私はSwift/Kotlinを書けませんしXcode/Android Studioをほぼ触ったことがありません。Expo Managed Workflowを使った開発の経験しかありません。

デバッグはもちろんiOS/Androidシミュレーターでもできますが、Expo Goアプリを利用して実機で簡単にできます。私は手持ちのMacBookAirよりも快適に使える自作パソコンで開発するのが好きなので、Windows上のVSCodeでコードを書く→iPhoneで実機デバッグするという形で開発していました。

OTAアップデートやホットリロードも完全に動作するため特に理由がなければExpoを使って開発するのがオススメです。

一方で、Expoには欠点もあります。それはReact Native CLIとは異なり**ネイティブコードを触ることができない**ということです。具体的にはBlutoothやWebRTC、InAppPurchaseなどネイティブコードを使う一部のライブラリには非対応です。ただ、GPSやカメラ、Mapなど大半の機能はExpoが純正ライブラリを提供しているためこれまでリリースしてきたアプリはExpoで開発できました。

## EAS CLI

Expo Application ServicesはExpoの提供する次世代のビルドサービスです。Expoの便利な機能を使いながら上述のこれまで非対応だったネイティブコードを触るライブラリを利用することができます。

2021年後半の本リリースに向けて現在はプレビュー機能となっていますが、**$29/mo**の[Priority Plan](https://expo.dev/pricing)を登録することで利用できるようになります。

## Ocacatalk

というわけで、実際にPriority Planに登録してEAS buidを使ってリリースしたのが冒頭で紹介した新作アプリ**Ocacatalk**です。

Ocacatalkのビデオチャット機能は[Agora](https://www.agora.io/en/)を利用しています。

Agoraは、自社開発のスマートフォン・PCアプリやWebサイトに、カスタマイズしたビデオ・音声通話やライブ配信をかんたんに実装できるSDKです。Clubhouseのバックエンドに使われていることでもおなじみのSaaSです。

React Native用の[ライブラリ(react-native-agora)](https://github.com/AgoraIO-Community/react-native-agora)はこれまでも提供されていましたが、AgoraのビデオチャットはWebRTCであり、Expoでは利用できませんでした。

OcacatalkはEAS buildを使い、これまでできなかったAgoraSDKを利用して開発しました。

#3. デモアプリを作ろう

## EAS buildの開発方法

EAS buildはまだプレビュー段階のためあまり情報がありません。Expo Managed Workflowでの開発に慣れていれば問題ないと思います。

しかし、いくつか違いがあるので実際にAgoraを使ってビデオチャットを行うデモアプリを作る方法を書きます。

デモアプリのリポジトリです。

- [kiyohken2000/agora-eas-demo](https://github.com/kiyohken2000/agora-eas-demo)

## 出来上がるもの

左がiPhone12、右がiPhone6sでとったスクリーンショットです。2つのデバイスでビデオチャットをしている様子です。

![img2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/bdfbeb42-4f13-8cd1-db14-4e61f330b970.jpeg)

## AgoraIDの準備

本題からはずれるので詳細は省きますが、とりあえず[Agora](https://www.agora.io/en/)にサインアップしてプロジェクトを作成します。**10,000 FREE mins per month**の無料枠があり、クレジットカード情報の登録も必須じゃないので安心です。

プロジェクトを作成してAppIDを取得します。デモアプリなのでApp certificateはDisableにしておきます。

![img3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/46284c34-41f7-815b-19fe-2b3bfac15c5c.png)

## 開発環境

ネイティブコードを利用する場合Expo Goアプリでデバッグできないので、iOSかAndroidのシミュレーターを使います。自作パソコンにAndroid Studioを入れたくないのでMacBookAirを使います。

- Mac Book Air 2020 (Intel, macOS 11.5.2)
- expo-cli v4.9.1
- eas-sli v0.23.0

react-native-cliは入っていません。

![img4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/a494d2dd-b8b3-0692-0ee7-05f26106da22.png)

## Expoプロジェクトの準備

`expo init`で新規で作ってもいいのですが、せっかくなのでアプリっぽくするために前に作った自分用のボイラープレートを使います。

- [kiyohken2000/reactnative-expo-firebase-boilerplate](https://github.com/kiyohken2000/reactnative-expo-firebase-boilerplate)

![img5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/7611c5ed-296b-38d1-e149-08ced454be63.jpeg)

- ボトムタブ、スタックナビゲーション
- Firebase authを使ったメールアドレス/PWでのサインアップ/ログイン
- Firestoreを使ったユーザープロフィールの編集
- Firebase Cloud Storageを使ったアバター画像のアップロード
- expo notifications tokenの取得とFirestoreへの保存
- デバイス設定に合わせたダークモードの動的切り替え

を実装した便利なボイラープレートです。

これをクローンしてきて依存関係をインストールします。

```shell
yarn install
```

とりあえず、Managed Workflowのまま動作確認します。

```shell
expo start
```

![img6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/6ac1aabc-868e-c028-e836-cd902524d484.jpeg)

無事に動きました。画像を載せる用にシミュレーターを使ってますがこの時点では実デバイスのExpo Goアプリでも動作します。

## app.jsonの編集

デモアプリに名前をつけます。

```javascript
"expo": {
  "name": "agora-eas-demo",
  "slug": "agora-eas-demo",
```

```javascript
"ios": {
  "supportsTablet": true,
  "bundleIdentifier": "net.votepurchase.agoraeasdemo",
  "buildNumber": "1.0.0",
  "infoPlist": {
    "NSPhotoLibraryUsageDescription": "Use the photo library to change your avatar.",
    "NSCameraUsageDescription": "Use for video call.",
    "NSMicrophoneUsageDescription": "Use for video call."
  },
  "userInterfaceStyle": "automatic"
},
```

`bundleIdentifier`を設定します。ビデオチャットにカメラとマイクを使うので`NSCameraUsageDescription`と`NSMicrophoneUsageDescription`も追加します。

```javascript
"android": {
  "package": "net.votepurchase.agoraeasdemo",
```

Androidのパッケージ名も設定します。

## EAS buildの準備

EASにログインします。Expoのユーザー名とパスワードです。

```shell
eas login
```

EASの初期化を行います。

```shell
eas build:configure
```

途中で色々聞かれますが**全てyes**を選択しました。

新しく**eas.json**が作成されます。

```javascript
{
  "build": {
    "release": {},
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    }
  }
}
```

デフォルトのままにしておきます。

## 動作確認

EASを使って動かしてみます。まだAgoraはインストールしていません。

iOSのシミュレーターを開いたまま以下のコマンドを打ちます。

```shell
expo run:ios
```

ルートディレクトリに`ios`フォルダが作成され、必要なファイルが作成されます。数分かかります。

しばらく待っているとビルドされたアプリが起動します。

![img7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/bdb650dd-28d5-0c8a-5b7f-5692190961f5.jpeg)

この時点では先ほどExpo Goで動かしたものと変わりありませんが、ホーム画面にはExpo Goアプリではなく今ビルドしたアプリがインストールされています。

![img8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/d8ee4553-321a-42ba-edf6-2d77146cbce8.jpeg)

今回のデモアプリではログイン後にビデオチャットの画面を置きたいのでアプリにログインしておきます。

![img9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/7dc3dba3-8ece-c66d-6301-db0f098ce70c.jpeg)

## Agora SDKのインストール

Agoraのライブラリをインストールします。

- [AgoraIO-Community/react-native-agora](https://github.com/AgoraIO-Community/react-native-agora)

```shell
yarn add react-native-agora
```

```shell
cd ios
```

```shell
pod install
```

UIはAgoraが用意してくれているものをそのまま利用します(ReactNative-UIKit)

- [AgoraIO-Community/ReactNative-UIKit](https://github.com/AgoraIO-Community/ReactNative-UIKit)

ルートディレクトリに戻ってからインストールします。

```shell
cd ..
```

```shell
yarn add agora-rn-uikit
```

以上でインストールは完了です。

## ページ遷移を作成

ホーム画面→チャット開始前→チャット開始後、という風に遷移したいので、チャット画面用のコンポーネントを作ります。

![img11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/a6f003e8-d3bf-4592-1eb2-87974ef62719.jpeg)

**src/scenes/stream/Stream.js**

```javascript
import React, { useEffect, useState } from 'react'
import { Text, View, ScrollView, StatusBar, useColorScheme, Button } from 'react-native'
import styles from './styles'
import { firebase } from '../../firebase/config'
import AgoraUIKit from 'agora-rn-uikit'

export default function Stream(props) {
  const userData = props.extraData
  const scheme = useColorScheme()
  const [videoCall, setVideoCall] = useState(false);
  const rtcProps = {
    appId: '<Agora App ID>',
    channel: 'easdemo',
  };
  const callbacks = {
    EndCall: () => setVideoCall(false),
  };

  return (
    <View style={styles.container}>
      <StatusBar barStyle="light-content" />
      <View style={{ flex: 1, width: '100%' }}>
        {
          videoCall ?
            <AgoraUIKit rtcProps={rtcProps} callbacks={callbacks} />
            :
            <Button title={'Start Call'} onPress={()=>setVideoCall(true)} />
        }
      </View>
    </View>
  )
}
```

`<Agora App ID>`には最初のほうで作成したAgoraのAppIDが入ります。

作ったコンポーネントをルーティングに追加します。

**src/routes/navigation/stacks/Stacks.js**

```javascript
import Stream from '../../../scenes/stream'
```

```javascript
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
  <Stack.Screen name="Home">
    {props => <Home {...props} extraData={user} />}
  </Stack.Screen>
  <Stack.Screen name="Stream">
    {props => <Stream {...props} extraData={user} />}
  </Stack.Screen>
</Stack.Navigator>
```

Home画面からチャット画面(Stream)に遷移できるようにします。

**src/scenes/home/Home.js**

```javascript
import { useNavigation } from '@react-navigation/native'
```

```javascript
const navigation = useNavigation()
```

```javascript
<Button title={'Go Stream'} onPress={() => {
  navigation.navigate('Stream')
}} />
```

コードの変更はここまでです。

## 動作確認

実際に動かしてみます。

```shell
expo run:ios
```

画面遷移などはできますが、ビデオチャットが開始できません。シミュレーターではカメラとマイクが使えないのでこうなります。

![img12.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/9249d359-b82f-b240-f59f-129a1dbea91d.jpeg)

## 動作確認用に実デバイスにインストール

ビデオチャットの動作確認をしたいのでXcodeを使って実デバイス用にビルドします。

ルートディレクトリの中の`ios`フォルダをXcodeで開きます。**Open a project or file**から先ほど作成された`ios`フォルダを選択します。

![img13.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/b96c3119-833b-3abe-747f-926dfd584e91.jpeg)

実機にインストールするためには署名をしないといけません。**Signing & Capabilities**タブを開いて**Team**を選択します。Appleのデベロッパーアカウントが必要です。

![img14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/f651d529-eaf1-e270-e1e4-5af420ae8b55.jpeg)

iPhoneをUSBケーブルでMacと接続します。インストール先を今接続したiPhoneにします。

![img15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/c3381ce7-8c32-1fb7-d667-8b01449556bf.jpeg)

左上のビルド開始ボタンをクリックするとビルドとインストールが開始されます。

![img16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/bb152e33-e959-b4f7-5be1-9e4a45a514d9.jpeg)

しばらく待ってるとインストールが完了します。物理iPhoneのスクリーンショットです。

![img17.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/546bf955-7bcf-a8fd-b313-7e6ab8bb8cad.jpeg)

## 実デバイスで動作確認

ビデオチャットの確認をしたいのでiPhone2台にインストールしました。それぞれのiPhoneでインストールしたデモアプリを起動すると無事にビデオチャットが開始されました。

![img2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/bdfbeb42-4f13-8cd1-db14-4e61f330b970.jpeg)

## リリースビルド

リリースするわけではないですが確認のため実際にリリースビルドしてみました。

```shell
eas build --platform ios
```

無事にビルドできました。あとは通常のExpoアプリと同じようにTransporterアプリを使ってAppStoreに提出します。

![img18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/482d5f40-eee3-96c2-c29b-bf263b2842af.png)

#4. 補足

### バージョン番号

バージョン番号のインクリメントは手動です。

Managed Workflowではストアに新バイナリを提出するときに**app.json**の`expo.ios.buildNumber`と`expo.android.versionCode`をインクリメントしますが、EAS buildでは手動でそれぞれ更新しないといけません。

iOSなら**ios/agoraeasdemo/Info.plist**の`CFBundleVersion`を手動でインクリメントします。

Androidは**android/app/build.gradle**の`android.defaultConfig.versionCode`です。

### Androidでのビルド

ここまでXcodeを使ったiOS用のビルドについてだけ書いてきました。Androidについては書いていません。

![img19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681892/8e45fd9d-18cf-d3f0-5e2b-a2f587672803.jpeg)


なぜならAndroid Studioのエミュレーターでどうしても動かせなかったからです。ただし、

```shell
eas build --platform android
```

でのリモートビルドは普通にできてしまったのでAndroidでの動作確認をしないままGoogle Playに提出してそのままリリースしました。

### OTAアップデート

Testflight中でもリリース後でも`expo publish`でのOTAアップデートはできました。ただし、通常のExpoアプリと同様に**app.json**の変更やネイティブコードを触るライブラリを追加した場合はOTAアップデートできません。ストア経由でのアップデートが必要です。

### AgoraとAppStoreレビュー

AgoraIOを利用したアプリではAppStoreのレビューに難があります。

- [How do I get past the AppleStore review process?](https://github.com/AgoraIO-Community/react-native-agora/issues/357)

レビュアーはVPNを通してレビューしているようですが、AgoraはVPN経由だとビデオチャットが開始できません。そのため「ビデオチャットが開始できない」という理由で何度もリジェクトされてしまいました。

最終的にはレビュアーへのメモに

```
I run the app on my real device using Testflight, the permission request is displayed without any problem and I can allow it to use the camera / microphone.

**Please allow the app to access the camera and microphone. If you do not allow it, you will not be able to video chat.**
**Use a real device. Cameras and microphones are not available in the simulator.**

I have captured it working on my device.
https://youtu.be/Dh5h1-Kk1eU

If you are using a VPN, turn it off.

>The AppStore review team is testing via VPN, so that's probably the problem, but I don't know much about networks, so I don't know what the solution is.
https://github.com/AgoraIO-Community/react-native-agora/issues/357
```

という風に

- VPNを使わないで欲しいこと
- 実デバイスを使って欲しいこと
- 自分のデバイスでは動作すること
- YouTubeにアップロードしたデモビデオのリンク

を書いたところレビューを通過しました。

#5. まとめ

EAS buildを利用することでExpo Managed Flowでこれまで作れなかったアプリが作れるようになりました。

Expo GoアプリでのデバッグができないためMacが必須ではありますが、Managed Workflowでの快適な開発体験をほとんど損なうことなくネイティブコードを触るライブラリを使えます。

Expoにはお世話になっているのでしばらく課金は続けようと思います。