🐕

AndroidでChatGPTのRealtimeAPIをWebRTCで利用する方法

2025/03/04に公開

はじめに

本記事では、AndroidでChatGPTのRealtimeAPIをWebRTCで利用するサンプルアプリを作った経験を共有します。特に、getstream.ioのWebRTCライブラリを使った実装方法について詳しく解説していきます。

なお、本記事で解説するコードは以下のGitHubリポジトリで公開しています:
https://github.com/Gazyu/RealtimeApiTestApp

ChatGPTのRealtimeAPIとは

ChatGPTのRealtimeAPIは、WebRTCプロトコルを使ってリアルタイムの音声対話を実現するAPIです。従来のREST APIとは異なり、双方向のリアルタイム通信が可能で、音声入力に対してほぼ遅延なく応答を得ることができます。

主な特徴:

  • 低遅延の双方向音声通信
  • WebRTCプロトコルの活用
  • データチャネルを通じたイベント通知

WebRTCとは

WebRTC(Web Real-Time Communication)は、ブラウザやモバイルアプリ間でプラグインなしにリアルタイム通信を可能にするオープンソース技術です。音声、ビデオ、データの送受信をP2P(ピアツーピア)で行うことができます。

WebRTCの主要コンポーネント:

  • PeerConnection:通信相手との接続を管理
  • DataChannel:テキストデータなどの送受信を担当
  • MediaStream:音声・ビデオストリームの管理

アプリの機能と使い方

今回作ったアプリは、AndroidでChatGPTのRealtimeAPIを使って音声対話ができるシンプルなサンプルです。

主な機能

  • OpenAI APIトークンを使った認証
  • WebRTCによるChatGPTとのリアルタイム音声通信
  • データチャネルを通じたイベント情報の受信
  • 接続状態のリアルタイム表示

使い方

  1. アプリを起動する
  2. OpenAI APIトークンを入力欄に入力する
  3. 「接続開始」ボタンをタップする
  4. マイク権限を許可する
  5. 接続が確立されると、ChatGPTと音声で会話できる
  6. 「通話終了」ボタンをタップして接続を終了する

動作イメージ

アプリのUIはシンプルで、トークン入力欄、接続/切断ボタン、ログ表示エリアだけです。接続状態やエラーメッセージ、受信したデータチャネルメッセージがログエリアにリアルタイムで表示されるので、何が起きているか一目でわかります。

技術的な解説

アプリの構成

このアプリは主に以下のコンポーネントで構成されています:

  • UI層:MainActivity、レイアウトファイル
  • WebRTC層:WebRtcClient、WebRtcManager(WebRTC接続の管理)
  • データ層:ApiService、モデルクラス(SessionConfig、SessionQueryなど)

getstream.ioのWebRTCライブラリ

今回の実装で特に重要なのは、getstream.ioが提供するWebRTCライブラリを活用した点です。このライブラリのおかげで、WebRTCの複雑な実装がかなり簡単になりました。

ライブラリ選定の理由

WebRTCライブラリとしてgetstream.ioのものを選んだ理由は以下の通りです:

  1. Google公式WebRTC SDKの問題:Google公式のWebRTC SDKはAndroid版がメンテナンスされておらず、実用的に使えない状況でした
  2. 最新コードベース:GetStream.ioのライブラリは本家WebRTCの最新コードに近い実装になっており、最新の機能や改善が含まれています
  3. Android向け最適化:AndroidSDKとして適切に最適化されており、Androidアプリでの利用がスムーズです
  4. 無料で利用可能:商用利用も含めて無料で使えるため、コスト面での障壁がありません

他のWebRTCライブラリも検討しましたが、上記の理由からgetstream.ioのライブラリが最も適していると判断しました。

ライブラリの特徴

getstream.ioのWebRTCライブラリには以下のような特徴があります:

  • Androidに最適化されたWebRTC実装
  • シンプルで使いやすいAPI設計
  • 安定した音声・ビデオ通信
  • 効率的なリソース管理
  • 充実したドキュメントとサポート

導入方法

build.gradleに以下の依存関係を追加するだけで簡単に導入できます:

dependencies {
    implementation 'io.getstream:stream-webrtc-android:1.x.x'
}

これだけで、WebRTCの機能が使えるようになります。

WebRTC実装の詳細

WebRTC実装は主にWebRtcClientWebRtcManagerの2つのクラスで構成されています。

WebRtcClient

WebRtcClientはgetstream.ioのライブラリを活用してWebRTCの低レベルな操作を担当します:

class WebRtcClient @Inject constructor(@ApplicationContext private val context: Context) {
    private var peerConnectionFactory: PeerConnectionFactory? = null
    private var peerConnection: PeerConnection? = null
    private var dataChannel: DataChannel? = null
    private var localAudioTrack: AudioTrack? = null

    fun initialize() {
        // getstream.ioのライブラリを使用したPeerConnectionFactoryの初期化
        PeerConnectionFactory.initialize(
            PeerConnectionFactory.InitializationOptions.builder(context)
                .createInitializationOptions()
        )
        val options = PeerConnectionFactory.Options()
        peerConnectionFactory = PeerConnectionFactory.builder()
            .setOptions(options)
            .createPeerConnectionFactory()
    }

    // PeerConnectionの作成
    fun createPeerConnection(observer: PeerConnection.Observer): PeerConnection? {
        val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
        rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN

        peerConnection = peerConnectionFactory?.createPeerConnection(rtcConfig, observer)
        return peerConnection
    }

    // オーディオトラックの作成
    fun createAudioTrack(): AudioTrack? {
        val audioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
        localAudioTrack = peerConnectionFactory?.createAudioTrack("localAudio", audioSource)
        localAudioTrack?.setEnabled(true)
        return localAudioTrack
    }

    // データチャネルの作成
    fun createDataChannel(): DataChannel? {
        dataChannel = peerConnection?.createDataChannel("oai-events", DataChannel.Init())
        return dataChannel
    }

    // その他のWebRTC関連メソッド...
}

WebRtcManager

WebRtcManagerWebRtcClientをラップし、より高レベルな操作と状態管理を提供します:

class WebRtcManager @Inject constructor(
    private val webRtcClient: WebRtcClient,
    private val okHttpClient: OkHttpClient
) {
    private var eventListener: WebRtcEventListener? = null
    private var isInitialized = false
    private var pendingOffer: String? = null

    // WebRTC接続のセットアップ
    fun setupConnection() {
        if (!isInitialized) {
            throw IllegalStateException("WebRtcManager must be initialized before setting up connection")
        }

        // PeerConnectionObserverの作成
        val observer = createPeerConnectionObserver()
        
        // PeerConnectionの作成
        webRtcClient.createPeerConnection(observer)
        
        // オーディオトラックの追加
        webRtcClient.createAudioTrack()
        webRtcClient.addAudioTrack()
        
        // データチャネルの作成
        val dataChannel = webRtcClient.createDataChannel()
        dataChannel?.registerObserver(createDataChannelObserver())
    }

    // SDPオファーの作成
    fun createOffer(onOfferCreated: (String) -> Unit) {
        val sdpObserver = object : SdpObserver {
            override fun onCreateSuccess(sessionDescription: SessionDescription) {
                pendingOffer = sessionDescription.description
                webRtcClient.setLocalDescription(this, sessionDescription)
            }

            override fun onSetSuccess() {
                pendingOffer?.let { offer ->
                    onOfferCreated(offer)
                }
            }
            
            // エラーハンドリング...
        }

        webRtcClient.createOffer(sdpObserver)
    }

    // その他のWebRTC管理メソッド...
}

ChatGPT RealtimeAPIとの連携方法

ChatGPT RealtimeAPIとの連携は主に2つのステップで行われます:

  1. セッションの取得:APIトークンを使ってセッション情報を取得
  2. WebRTC接続の確立:SDPオファーを作成してAPIに送信し、応答を処理

セッションの取得

private suspend fun getEphemeralKey(token: String) = withContext(Dispatchers.IO){
    _state.value = MainState.Loading

    try {
        val result = sessionRepository.getSession(
            token,
            "gpt-4o-realtime-preview-2024-12-17",
            "verse",
            "日本語で会話をしてください。"
        )

        result.onSuccess { config ->
            _state.value = MainState.TokenReceived(config.clientSecret.value)
            initRtc(config.clientSecret.value)
        }.onFailure { error ->
            _state.value = MainState.Error("トークン取得エラー: ${error.message}")
        }
    } catch (e: Exception) {
        _state.value = MainState.Error("予期せぬエラー: ${e.message}")
    }
}

WebRTC接続の確立

private fun sendOfferToOpenAI(token: String, offer: String) {
    viewModelScope.launch {
        _state.value = MainState.SendingOffer

        try {
            val response = withContext(Dispatchers.IO) {
                val url = "https://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-12-17"
                val request = Request.Builder()
                    .url(url)
                    .addHeader("Authorization", "Bearer $token")
                    .addHeader("Content-Type", "application/sdp")
                    .post(offer.toByteArray().toRequestBody())
                    .build()

                webRtcManager.getHttpClient().newCall(request).execute()
            }

            if (response.isSuccessful) {
                val answerSdp = response.body?.string() ?: ""
                webRtcManager.setRemoteDescription(answerSdp)
                _state.value = MainState.Connected
            } else {
                _state.value = MainState.Error("OpenAIからの応答エラー: ${response.code}")
            }
        } catch (e: Exception) {
            _state.value = MainState.Error("オファー送信エラー: ${e.message}")
        }
    }
}

実装のポイント

getstream.ioライブラリを活用したWebRTCセットアップの流れ

getstream.ioのライブラリを使ったWebRTCセットアップの基本的な流れは以下の通りです:

  1. PeerConnectionFactoryの初期化(通信の基盤を作る)
  2. PeerConnectionの作成(通信相手との接続準備)
  3. オーディオトラックの作成と追加(音声を送れるようにする)
  4. データチャネルの作成(テキストデータをやり取りする準備)
  5. SDPオファーの作成(通信提案の作成)
  6. ローカル記述の設定(自分側の通信設定)
  7. オファーのOpenAIへの送信
  8. OpenAIからの応答(SDP応答)の処理
  9. リモート記述の設定(相手側の通信設定)

この流れをきちんと実装することで、ChatGPTのRealtimeAPIとの接続が確立されます。

セッション管理の方法

セッション管理は状態管理クラスを使って実装しました:

sealed class MainState {
    object Initial : MainState()      // 初期状態
    object Loading : MainState()      // ロード中
    data class TokenReceived(val token: String) : MainState()  // トークン取得完了
    object SendingOffer : MainState() // オファー送信中
    object Connected : MainState()    // 接続完了
    object Disconnected : MainState() // 切断
    data class Error(val message: String) : MainState() // エラー発生
}

この状態管理により、UIは常に現在の接続状態を反映し、ユーザーに適切なフィードバックを提供できます。

エラーハンドリング

WebRTCとAPIの連携では様々なエラーが発生する可能性があります。このアプリでは、以下のようなエラーハンドリングを実装しています:

  • APIリクエストのエラー処理
  • WebRTC接続エラーの処理
  • SDP交換中のエラー処理
  • 接続状態変化の監視

エラーが発生した場合は、ユーザーに分かりやすいメッセージを表示し、適切な状態に遷移します。これがないと、アプリが突然停止したり、フリーズしたりする原因になります。

コード解説

重要な実装部分のハイライト

getstream.ioライブラリを使用したWebRTC接続の初期化

fun initialize() {
    // getstream.ioのライブラリを使用した初期化
    PeerConnectionFactory.initialize(
        PeerConnectionFactory.InitializationOptions.builder(context)
            .createInitializationOptions()
    )
    val options = PeerConnectionFactory.Options()
    peerConnectionFactory = PeerConnectionFactory.builder()
        .setOptions(options)
        .createPeerConnectionFactory()
}

データチャネルメッセージの処理

getstream.ioのライブラリを使用したデータチャネルの処理:

private fun createDataChannelObserver(): DataChannel.Observer {
    return object : DataChannel.Observer {
        override fun onMessage(buffer: DataChannel.Buffer) {
            val msgBytes = ByteArray(buffer.data.remaining()).also { buffer.data.get(it) }
            val message = String(msgBytes, Charsets.UTF_8)
            Log.d(TAG, "DataChannel message: $message")
            eventListener?.onDataChannelMessage(message)
        }

        override fun onStateChange() {
            Log.d(TAG, "DataChannel state changed")
        }

        override fun onBufferedAmountChange(previousAmount: Long) {
            // バッファ量の変更は通常ログに記録する必要はない
        }
    }
}

PeerConnectionObserverの実装

getstream.ioのライブラリを活用したPeerConnectionObserverの実装:

private fun createPeerConnectionObserver(): PeerConnection.Observer {
    return object : PeerConnection.Observer {
        override fun onIceCandidate(candidate: IceCandidate?) {
            Log.d(TAG, "onIceCandidate: $candidate")
        }

        override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
            Log.d(TAG, "onConnectionChange: $newState")
            newState?.let { eventListener?.onConnectionStateChange(it) }
        }

        override fun onDataChannel(dataChannel: DataChannel?) {
            Log.d(TAG, "onDataChannel")
            dataChannel?.registerObserver(createDataChannelObserver())
        }

        // 他のコールバックメソッドの実装...
        override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array<out MediaStream>?) {
            Log.d(TAG, "onAddTrack")
            mediaStreams?.firstOrNull()?.audioTracks?.firstOrNull()?.setEnabled(true)
        }
    }
}

OpenAIへのオファー送信

val url = "https://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-12-17"
val request = Request.Builder()
    .url(url)
    .addHeader("Authorization", "Bearer $token")
    .addHeader("Content-Type", "application/sdp")
    .post(offer.toByteArray().toRequestBody())
    .build()

webRtcManager.getHttpClient().newCall(request).execute()

getstream.ioライブラリを使用する際の注意点

getstream.ioのWebRTCライブラリを使用する際には、以下の点に注意が必要です:

  1. 適切なバージョンの選択:ライブラリのバージョンによって互換性が異なるため、適切なバージョンを選ぶ必要があります
  2. リソースの解放:WebRTC接続が不要になったら、適切にリソースを解放することが重要です
  3. 権限の管理:マイク権限などの必要な権限をきちんと管理する必要があります
  4. エラーハンドリング:WebRTC接続中に発生する可能性のあるエラーを適切に処理する必要があります

特にリソース解放は重要です:

@Synchronized
fun dispose() {
    dataChannel?.dispose()
    peerConnection?.dispose()
    peerConnectionFactory?.dispose()
    dataChannel = null
    peerConnection = null
    peerConnectionFactory = null
}

デモ

まとめ

とりあえずコードからだいたいこんな記事かけるAIすごい

ソースコード

本記事で解説したサンプルアプリのソースコードは、以下のGitHubリポジトリで公開しています。ぜひ参考にしてみてください:

https://github.com/Gazyu/RealtimeApiTestApp

Discussion