10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

agora.ioで人狼アプリにボイスチャット機能を実装してみた

Last updated at Posted at 2021-06-25

チャットで人狼をするアプリを2013年から運用しています。
Clubhouseが利用していることで話題になったagora.ioのSDKを使って、ボイスチャットで人狼ができたらおもしろいんじゃないかと思い、実装することにしました。

やりたいこと

人狼アプリという性質上、次のような実装を目指します。

  • 昼間の時間は生存している人のみが発言できる
  • 死亡した人は生存者の会話は聞けるが発言はできない
  • 死亡した人は死亡した人同士でいつでも会話できる
  • 人狼が複数いる場合、夜の時間は人狼同士で会話できる
  • ゲームに参加していない人は、生存している人の会話を観覧(?)できる

agora.ioについて

agora.ioは手軽にビデオチャットやボイスチャットができるようになるサービスです。
サービスの登録方法、SDKの導入方法につきましては割愛します。
今回はiOSアプリに導入してみました。

マルチチャンネル接続する

agoraについての記事はだいたいシングルチャンネル接続について解説しているようです。
今回の要件ではマルチチャンネル接続をしたかったのですが、あまりドキュメントがなかったので解説していきます。

こちらは公式ドキュメントのビデオのほうでマルチチャンネル接続する方法です。
ボイスの場合も基本的には同じです。

注意点としては、ChannelProfile.liveBroadcastingにしないといけません。
初期化コードは次のようにしました。

self.agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: agoraAppId, delegate: self)
self.agoraKit?.disableVideo()
self.agoraKit?.setAudioProfile(self.audioProfile, scenario: self.audioScenario)
self.agoraKit?.setChannelProfile(.liveBroadcasting)
self.agoraKit?.enableAudioVolumeIndication(200, smooth: 3, report_vad: true)

生存者のチャンネルにジョインする

生存者のチャンネルは、昼の間は生存者はbroadcaster、それ以外の人はaudienceとしてジョインします。
マルチチャンネルの場合、誰かがすでに作成したチャンネルにジョインするのではなく、すべての人が自分でチャンネルを作成してそこにジョインします。
チャンネル名が同じ場合は同じチャンネルとして扱われますので、チャンネルがたくさんできてしまうというわけではありません。

生存者のチャンネルにジョインするコードは以下のようになりました。

private func doJoinAliveChannel(channelId: String, playerId: String, isAudience: Bool, token: String) {
    guard let agoraKit = self.agoraKit else {
        return
    }

    if let channel = agoraKit.createRtcChannel(channelId) {
        channel.setRtcChannelDelegate(self)
        if isAudience {
            channel.setClientRole(.audience)
            print("Joining Alive channel as audience.")
        } else {
            channel.setClientRole(.broadcaster)
            print("Joining Alive channel as broadcaster.")
        }
        channel.publish()

        let mediaOptions = AgoraRtcChannelMediaOptions()
        mediaOptions.autoSubscribeAudio = true

        let result = channel.join(byUserAccount: playerId, token: token, options: mediaOptions)
        if result != 0 {
            print("Error joining channel: \(channelId) \(result)")
            return
        }
        self.aliveChannel = channel
    }
}

死者の場合は同じように死者のチャンネルにbroadcasterとしてジョインします。
マルチチャンネルなので同時にジョインできます。

夜の時間になったら、一旦生存者チャンネルをクローズして、人狼の人のみを人狼チャンネルに接続させます。

delegateを設定する

こちらが今回一番苦労した部分です。
公式ドキュメントはだいたいシングルチャンネルについて書かれているので、試行錯誤しました。

delegateは、agoraの動作、およびシングルチャンネルに関するdelegateのAgoraRtcEngineDelegateと、マルチチャンネルの各チャンネルに対するメッセージを受け取る AgoraRtcChannelDelegateに分かれています。

それぞれの役割について解説します。

AgoraRtcEngineDelegate

マルチチャンネルの場合、使用するのは以下の3つです

// agoraエンジンでWarningがあった場合
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) {
    print("warning: \(warningCode.rawValue)")
}
// agoraエンジンでerrorがあった場合
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
    print("error: \(errorCode.rawValue)")
}

大体の場合、UIで誰が発言したかのインジケーターを表示したいと思いますが、その場合はこちらを実装します。
agoraの初期化の際に enableAudioVolumeIndication を設定しておいてください。

func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
    for speaker in speakers {
        // speaker.uid の人が speaker.volume の音量で発言した旨の表示をここで行う
    }
}

AgoraRtcChannelDelegate

マルチチャンネルの場合、ほとんどのメッセージはこちらのdelegateを使います。

Local user (自分) に関するメッセージ

// Local user(自分)がチャンネルにジョインした
func rtcChannelDidJoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("Joined: \(channelId)  \(uid)")
}
// Local user(自分)が再びチャンネルにジョインした
func rtcChannelDidRejoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("ReJoined: \(channelId)  \(uid)")
}
// Local user(自分)がチャンネルから離れた (呼ばれない?)
func rtcChannelDidLeave(_ rtcChannel: AgoraRtcChannel, with stats: AgoraChannelStats) {
    print("DidLeave: \(rtcChannel.getId() ?? "")  \(stats)")
}
// Local user(自分)がネットワークなどの原因で切断された
func rtcChannelDidLost(_ rtcChannel: AgoraRtcChannel) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("DidLost: \(channelId)")
}

Remote user (他人) に関するメッセージ

// Remote userにwarningが起きた
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurWarning warningCode: AgoraWarningCode) {
    print("channel: \(rtcChannel.getId() ?? ""), warning: \(warningCode.rawValue)")
}

// Remote userにerrorが起きた
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurError errorCode: AgoraErrorCode) {
    print("channel: \(rtcChannel.getId() ?? ""), Error: \(errorCode.rawValue)")
}

broadcasterの人がジョインしたときはこちらが呼ばれます。 audience の人がジョインした時にはメッセージは来ないようなので、agoraのReal-time Messaging SDKなどを利用して自前でやる必要があります。

// broadcaster の人がJoinした
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didJoinedOfUid uid: UInt, elapsed: Int) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("didJoinedOfUid: \(channelId) \(uid)")

    var error: AgoraErrorCode = .noError
    if let info = agoraKit?.getUserInfo(byUid: uid, withError: &error) {
        if error == .noError, let userAccount = info.userAccount {
            // joinした時の処理をここで行う
        }
    }
}
// remote userの人がオフラインになった
func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("didOfflineOfUid: \(channelId) \(uid) reason \(reason.rawValue)")

    var error: AgoraErrorCode = .noError
    if let info = agoraKit?.getUserInfo(byUid: uid, withError: &error) {
        if error == .noError, let userAccount = info.userAccount {
            //オフラインになったときの処理をここで行う
        }
    }
}

Remote userの人がマイクをミュートしたり解除したりした場合に呼ばれます。
Joinedが呼ばれずにいきなりこちらが呼ばれる場合もあるので、そちらも想定して実装しましょう。

func rtcChannel( _ rtcChannel: AgoraRtcChannel, remoteAudioStateChangedOfUid uid: UInt,
                     state: AgoraAudioRemoteState, reason: AgoraAudioRemoteStateReason, elapsed: Int
    ) {
    guard let channelId = rtcChannel.getId() else {
        return
    }
    print("remoteAudioStateChangedOfUid: \(channelId) \(uid) reason \(reason.rawValue)")

    if state == .stopped || state == .starting {
        let isMute = (state == .stopped)
        // MuteのUI表示などの処理をここで行う。
        // Joinedが呼ばれなかった人の処理もここで。
    }
}

終了時の処理

チャンネルを終了する時の処理です

self.aliveChannel?.leave()
self.aliveChannel?.unpublish()
self.aliveChannel?.destroy()

人狼アプリについて

今回実装したアプリはこちらです。
要望が多ければAndroid版やWeb版にも実装していきたいです。

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?