Agora.ioを使ったAndroidの音声配信！

こんにちは、AWAでAndroidアプリ開発をしている佐藤です！
このたび、AWAで音声配信機能がリリースされました :tada: 

https://www.cyberagent.co.jp/news/detail/id=27670

AWAの音声配信は自前で全て実装しているわけではなく、 [Agora.io](https://www.agora.io/en/) を使って実現しています。
そこで、Agora.ioを使って音声配信機能を実現する上での簡単なサンプルと、いくつかのハマりどころについて記載しようと思います。

# 前置き

- 使用するバージョンは `io.agora.rtc:voice-sdk:3.5.0` です
- 音声配信のみです。動画配信はしていません
- Agora.ioへのユーザ登録は既に済ませているものとします

# 実装する上で

Agoraはとてもサンプルが充実しており、日々アップデートされています。そのため、まずはサンプルを使って出来ることを把握しつつ、アプリに取り込む作業を行っていくと良いと思います。Androidのサンプルは以下にまとまっているので確認してみてください。

https://github.com/AgoraIO/API-Examples/tree/master/Android/APIExample

サンプルアプリはUseCaseごとに画面を分けている作りになっていて、とりあえず音声配信を試したいのであれば [Audio live streaming](https://github.com/AgoraIO/API-Examples/blob/master/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java) を参考にすると良いです。

## 簡単なサンプル

RtcEngineを作成して、joinChannelすることで音声配信が始まります。簡単ですね。
音声配信時になにか起きると作成時のパラメータである `IRtcEngineEventHandler` にイベントが飛んでくるので、適宜処理を行いましょう。

```kotlin
    val engine = RtcEngine.create(context, applicationId, iRtcEngineEventHandler).apply {
        // 音声配信を聞くだけなら CLIENT_ROLE_AUDIENCE でもよい
        setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER)
    }
    val option = ChannelMediaOptions().apply {
        autoSubscribeAudio = true
        autoSubscribeVideo = false
    }
    engine.joinChannel(
        token,
        channelName,
        "Extra Optional Data",
        userId,
        option
    )
```

## Agoraに接続するためのTokenについて

AgoraのTokenはクライアントではなくサーバで発行しなければなりません。Tokenの発行方法については以下のリンクが参考になります。

https://docs.agora.io/en/Voice/token_server?platform=All%20Platforms#deploy-a-token-server

```go
func generateRtcToken(int_uid uint32, channelName string, role rtctokenbuilder.Role){
   ...
}
```

AccessTokenは発行時に指定されたUserId、ChannelName、roleでしか使用することができません。注意しましょう。
音声配信をとりあえず試したいと言うのであれば、TemporaryTokenを発行することもできます。[ここ](https://docs.agora.io/en/Voice/run_demo_voice_call_android?platform=Android#3-generate-a-temporary-token)にTokenの発行方法が記載されていますが、Channel名さえ決めてしまえば簡単に発行することができます。ただし、注意書きにもある通り本番環境で使うことはできませんので、本番環境で使う場合はちゃんとToken発行用のサーバを作りましょう。


# ハマりどころをいくつか

上記のサンプルでも分かる通り、Agora.ioで音声配信を行うのは非常に簡単です。しかし、簡単と言いつつもいくつかハマってしまったポイントがあったため、備忘録の意味を込めて記載しておきます。


## UserIdが負の値になる

Agora.io上で管理されているUserIdは `uint32` ですが、Javaには `unsigned` という概念が存在しません。なので、AndroidのAgoraSDKで使用されるUserIdは `Int` に変換された値が使われます。正の値の場合は問題ありませんが、負の値の場合はどこかで変換して上げる必要があるので注意しましょう。私はまんまとハマりました。

ちなみに、以下のリンクに書かれている通り、まだβ版ではありますがUserIdに文字列を使うこともできます。β版を本番環境で使うのはちょっと怖いですし、正式対応が早くされないかなと心待ちにしています。

[-uidに文字列は使用できますか？-](https://vcube-support.zendesk.com/hc/ja/articles/900002433666-uid%E3%81%AB%E6%96%87%E5%AD%97%E5%88%97%E3%81%AF%E4%BD%BF%E7%94%A8%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%81%8B-)

## 音声配信者の音量の取得方法

音声配信者の誰が喋っているのかがわかるようなUIを作ろうとすると誰が喋っているかをリアルタイムに知る必要があります。Agoraにはリアルタイムで音量を通知してくれるイベントが存在するので、簡単に実装することができます。詳しくは [ここ](https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler.html#a4d37f2b4d569fa787bb8c0e3ae8cd424) に記載されています。

```kotlin
    private val iRtcHandler = object : IRtcEngineEventHandler() {
        override fun onAudioVolumeIndication(
            speakers: Array<out AudioVolumeInfo>?,
            totalVolume: Int
        ) {
            Timber.v("volumes: $volumes")
        }
    }

    val engine = RtcEngine.create(context, applicationId, iRtcEngineEventHandler).apply {
        enableAudioVolumeIndication(interval, smooth, isReportVad)
    }
```

前述している通りAgora.ioで管理されているUserIdは `uint32` ですが、通知されるUserIdは `uint32` を `Int` に変換した値となります。サービス上のUserIdとAgora.io上のUserIdの紐付けには注意しましょう。

音声配信を行っている自分のボリュームを取得したい場合は `isReportVad` を true にします。自分の音量通知はAgora.io上のUserIdではなく、`0` で通知されます。詳しくは [ここ](https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_audio_volume_info.html) に記載されています。

## 音声配信のボリューム

音声配信で使用される端末のvolumeは、メディアvolumeと通話volumeの2種類を選ぶことができ、ProfileのScenarioによって決まります。詳しくは [ここ](https://docs.agora.io/en/Voice/faq/system_volume) に記載されています。


```kotlin
    val engine = RtcEngine.create(context, applicationId, iRtcEngineEventHandler).apply {
        setAudioProfile(
            Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY,
            Constants.AUDIO_SCENARIO_GAME_STREAMING
        )
    }
```

シナリオによってはBroadcasterとAudienceで使用されるVolume設定が違うことがあります。そのため、つなぐチャンネルでRoleが変わると使用されるVolume設定が変わってしまうことがあり、いきなり爆音で音声配信が流れたりします。注意しましょう。


## 音量が小さく感じる

まずやるべきはSpeakerPhoneが使われるように変更することです。

```kotlin
    val engine = RtcEngine.create(context, applicationId, iRtcEngineEventHandler).apply {
        setDefaultAudioRoutetoSpeakerphone(true)
    }
```

それでもだめな場合はボリューム調整関数によって Input / Output の音量を調整してみましょう。音量を大きくしすぎるとすぐに音割れが起こってしまうため慎重に調整する必要があるようです。

```kotlin
    val engine = RtcEngine.create(context, applicationId, iRtcEngineEventHandler).apply {
        // Input Volume
        adjustRecordingSignalVolume(RECORDING_VOLUME)
        // Output Volume
        adjustPlaybackSignalVolume(PLAYBACK_VOLUME)
    }
```

QAにも記載されている一般的な問題なようですが様々な原因が考えられるので一言で答えるのは難しいようです。音声配信に使用している端末にもよるようなので、サポートデスクに対して直接問い合わせてみるほうが良いかもしれません。

[-他拠点の音声がとても小さいですが-何が原因ですか-](https://vcube-support.zendesk.com/hc/ja/articles/360036042712-%E4%BB%96%E6%8B%A0%E7%82%B9%E3%81%AE%E9%9F%B3%E5%A3%B0%E3%81%8C%E3%81%A8%E3%81%A6%E3%82%82%E5%B0%8F%E3%81%95%E3%81%84%E3%81%A7%E3%81%99%E3%81%8C-%E4%BD%95%E3%81%8C%E5%8E%9F%E5%9B%A0%E3%81%A7%E3%81%99%E3%81%8B-)

# まとめ

何点かハマりどころ（僕が勝手にハマっただけかもしれません）はありましたが、Agora.ioを使ったことでとても少ない工数でAWAに音声配信を組み込むことができました。また、困ったときに日本語で問い合わせることが出来る[サポート](https://vcube-support.zendesk.com/hc/ja)があるところも非常に助かりました。

簡単な音声配信機能を既存アプリへ組み込むだけであれば数日で実装することができましたし、アプリに音声配信機能をお試しで組み込みたい場合にはおすすめできると思います。