【WebRTCを活用してAndroidで実装してみた】SDKで簡単に開発するには

WebRTC Android

Androidアプリにビデオ通話機能を導入したいけど、「WebRTCの実装って難しそう……」と感じていませんか?

この記事では、WebRTCプラットフォームを運営するSkyWayが、SkyWay SDKを活用し、簡単にAndroidのビデオ通話アプリを実装する方法を、具体的な実装手順とサンプルを通してわかりやすく紹介します。

 

 

代表的なビデオ通話プラットフォームとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。 「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。⇒概要資料をダウンロードする(無料)

WebRTCとは

WebRTC(Web Real-Time Communication)は、ウェブブラウザ間で音声・映像・データをリアルタイムに通信するための技術です。P2P方式を採用しており、サーバーを介さず低遅延な通信が可能で、ビデオ通話やライブ配信、データ共有などに幅広く活用されています。

詳細な解説は以下の記事をご参照ください。

skyway.ntt.com

AndroidでWebRTCを活用したビデオ通話アプリを簡単に実装する

今回は、Jetpack ComposeのサンプルであるJetChatに、WebRTCによる通話機能を実装します。

https://github.com/android/compose-samples/tree/main/Jetchat

完成したビデオ通話アプリ

完成したアプリは、以下のように利用することができます。

WebRTC Android ビデオ通話アプリ

  1. アプリを起動すると、チャット画面が表示されます。ビデオチャットボタンを押します。
  2. 自分が入室され、映像と音声の配信が開始します。
  3. 他のメンバーが入室、自動的に相手の映像と音声の受信が開始します。

実装済みのGitHubをチェックする

なお、以下のリンクから実装済みのリポジトリを確認することができますので、参考にしてください。

https://github.com/Jin-NeVen/SkyWayJetChat

動作環境

  • Android Studio Meerkat | 2024.3.1
  • Android 5.0 Lollipop(API Level 21)以降

事前準備

※SkyWay への登録がまだの方はこちらから

SkyWay コンソールへログインし、以下の 3 つを行います。

  1. 「アプリケーションを作成」ボタンを押す

    アプリケーション作成

  2. アプリケーション名を入力して作成ボタンを押す

  3. アプリケーション一覧からアプリケーション ID とシークレットキーをコピーする

実装手順

SkyWay Android SDKの導入

JetChatサンプルアプリを入手します。

libs.versions.tomlapp/build.gradle.kts を開き、SkyWay Android SDKの依存関係を追加します。

libs.versions.toml

[versions]
...
# 2025.4.3時点で最新版 skyway android sdkを導入します
skyway_room = "2.7.0"

[libraries]
...
skyway-room = { module = "com.ntt.skyway:room", version.ref = "skyway_room" }

app/build.gradle.kts

dependencies {
  ...
  implementation(libs.skyway.room)
  ...
}

Permissionの追加

WebRTCによるビデオ通話機能を利用するには、カメラおよびマイクのPermissionが必要です。Manifestファイルに必要なPermissionsを追加し、必要に応じて権限の要求を行います

AndroidManifest.xml

<manifest ...>
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.INTERNET" />
    
    <application ...>
    ...
    </application>

CheckPermissionsUseCase

権限要求の処理を記述しています。

一般的な処理であるため、この記事ではコードの紹介を割愛します。

NavActivity

JetChatサンプルアプリのMainActivityにおいて、 onCreate() 関数から checkPermissionsUseCase を呼び出すことで必要な権限を要求します。

class NavActivity : AppCompatActivity() {
    private val checkPermissionsUseCase = CheckPermissionsUseCase()
    
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets -> insets }

        checkPermissionsUseCase(this, listOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        ))
        ...
    }
}

SkyWay Android SDKの初期化

VideoChatViewModel.initializeSkyWay

SkyWay Android SDKを初期化するには、SkyWayAuthTokenが必要です。

AuthTokenはサーバー側で生成したものをクライアントに渡して使うことが想定されています。 しかし、サーバーの実装には手間がかかるので、今回は別の方法で初期化を行います。

SkyWay Android SDKでは、開発時にはAuthTokenを生成することなくSDKの機能を確認できるように、 SkyWayContext.setupForDev APIを提供しています。

このAPIを使えば、SkyWay Consoleで作成した AppIDと SecretKey によって初期化が可能です。

なお、 SkyWayContext.setupForDev はdev環境での利用が想定されているAPIです。SecretKeyを秘匿するため、本番環境でのご利用は控えてください。

SkyWayの初期化に成功したら、以下の手順でビデオ通話機能の実装を行います。

  1. ビデオチャットRoomを作成します
  2. チャットRoomメンバーを作成して入室します
  3. 映像と音声をチャットRoom内で配信します
  4. 他のチャットRoomメンバーの映像・音声を受信します
private suspend fun initializeSkyWay(applicationContext: Context) {
    if (SkyWayContext.setupForDev(applicationContext, TODO("AppID"), TODO("SecretKey"))) {
        Log.d(TAG, "skyway setup succeed")
        createRoom()
        createMemberMeAndJoinChatRoom()
        captureMyVideoSteam(applicationContext)
        captureMyAudioStream()
        publishMyAVStream()
        subscribeRoomMembersAVStream()
    }
}

ビデオチャットRoomを作る

VideoChatViewModel.createRoom

SFURoom.findOrCreateにより、ビデオチャットRoomを作成します。

必要に応じて、チャットRoomの各種Listenerをセットします。

他のメンバーが入室する際に、Room内のメンバーリストを更新する必要があるため、今回は onMemberListChangedHandler を利用します。

private suspend fun createRoom() {
    Log.d(TAG, "create video chat room")
    /**
     * NOTICE
     * 必要に応じてチャットRoomの名前を変えてください
     */
    val chatRoomName = "VideoChatRoom"
    chatRoom = SFURoom.findOrCreate(chatRoomName)
    if (chatRoom == null) {
        Log.d(TAG, "failed to create/find video chat room")
        return
    }
    Log.d(TAG, "video chat room created/found")
    chatRoom?.let { room ->
        room.onMemberListChangedHandler = {
            Log.d(TAG, "member list changed")
            //ここでmemberを取得してlistを更新する
            syncMembers(room.members)
        }
    }
}

ビデオチャットRoomに入室する

VideoChatViewModel.createMemberMeAndJoinChatRoom

RoomMember.Initを利用して、RoomMemberとしての自身の情報を作成します。

そして、Room.joinを呼び出して入室します。

入室に成功した場合は、 Room.join から、 LocalRoomMember が返ります。

LocalRoomMemberの利用可能なAPIはこちらをご参照ください。

private suspend fun createMemberMeAndJoinChatRoom() {
    if (chatRoom == null) {
        Log.d(TAG, "video chat room not created/found")
        return
    }

    chatRoom?.let {
        memberMe = it.join(RoomMember.Init(MemberRepository.memberMeName))
        if (memberMe == null) {
            Log.d(TAG, "member me join video chat room failed")
        } else {
            MemberRepository.memberMeId = memberMe!!.id
            if (members.find { it.id == MemberRepository.memberMeId } == null) {
                members = members + Member(name = memberMe!!.name, id = memberMe!!.id, isMe = true)
                Log.d(TAG, "member me(${MemberRepository.memberMeName}) join video chat room succeed")
            }
        }
    }
}

ビデオチャットRoomで自分の映像と音声の配信

VideoChatViewModel.captureMyVideoSteam

CameraSource.getCamerasを呼び出し、デバイスの端末一覧を取得します。

その後、CameraSource.startCapturingを利用してデバイスから映像のキャプチャを開始します。

最後にCameraSource.createStreamを利用し、映像配信用のLocalVideoStreamを作成します。

private suspend fun captureMyVideoSteam(context: Context) {
    val me = members.find { it.id == MemberRepository.memberMeId }

    me?.let {
        Log.d(TAG, "capture my video stream")
        val cameraList = CameraSource.getCameras(context).toList()
        val cameraOption = CameraSource.CapturingOptions(400,300)
        CameraSource.startCapturing(context, cameraList[0], cameraOption)
        withContext(Dispatchers.Main) {
            it.videoStream.value = CameraSource.createStream()
        }
    }
}

VideoChatViewModel.captureMyAudioStream

AudioSource.start APIを利用し、音声キャプチャを開始します。

そして、AudioSource.createStream APIを利用して音声配信用のLocalAudioStreamを作成します。

private fun captureMyAudioStream() {
    val me = members.find { it.id == MemberRepository.memberMeId }
    me?.let {
        Log.d(TAG, "capture my audio stream")
        AudioSource.start()
        it.audioStream.value = AudioSource.createStream()
    }
}

VideoChatViewModel.publishMyAVStream

入室して得られたLocalRoomMemberを利用して、上記で作成した LocalVideoStreamLocalAudioStream をチャットRoom内に配信します。

private suspend fun publishMyAVStreamInternal() {
    val me = members.find { it.id == MemberRepository.memberMeId }
    if (memberMe == null || me == null) {
        Log.d(TAG, "member me is null")
        return
    }
    if (me.videoStream.value != null) {
        Log.d(TAG, "member me publish video")
        memberMe!!.publish(me.videoStream.value as LocalVideoStream)
    }
    if (me.audioStream.value != null) {
        Log.d(TAG, "member me publish audio")
        memberMe!!.publish(me.audioStream.value as LocalAudioStream)
    }
}

ビデオチャットRoomで他のRoomメンバーの映像音声の受信

VideoChatViewModel.subscribeRoomMembersAVStream

LocalRoomMember.subscribe を呼び出し、他のRoomメンバーの映像・音声を受信します。

以下の二つのタイミングで受信を行います。

  • 入室時、チャットRoom内すでに配信されてる他のRoomMemberの映像・音声を受信します
  • Room.onStreamPublishedHandler にて、後から入室した他のRoomメンバーの映像・音声を受信します。
private fun subscribeRoomMembersAVStream() {
    if (chatRoom == null) {
        Log.d(TAG, "video chat room not created/found")
        return
    }

    chatRoom?.let { room ->
        //入室時、チャットRoom内すでに配信されてる他のRoomMemberの映像・音声を受信します
        room.publications.forEach { publication ->
            Log.d(TAG, "subscribe  ${publication.publisher?.name} 's ${publication.stream?.contentType} stream directly by publications id: ${publication.id},")
            subscribeRoomMembersAVStreamInternal(publication)
        }
        //自動的に後から入室された他のRoomMemberの映像と音声を受信します
        room.onStreamPublishedHandler = streamPublishedHandler
    }
}

private var streamPublishedHandler: ((publication: RoomPublication) -> Unit)? = {
    Log.d(TAG, "subscribe stream: publication id:${it.id}, publisher name: ${it.publisher?.name}")
    subscribeRoomMembersAVStreamInternal(it)
}

private fun subscribeRoomMembersAVStreamInternal(publication: RoomPublication) {
    viewModelScope.launch {
        if (chatRoom == null || memberMe == null) {
            return@launch
        }
        //自分が配信した映像・音声を無視する
        if (publication.publisher?.id == memberMe!!.id) {
            Log.d(TAG, "ignore my own publication")
            return@launch
        }
        //他人が配信した映像・音声を受信する
        val subscription = memberMe!!.subscribe(publication)
        if (subscription == null) {
            Log.d(TAG, "subscription is null")
            return@launch
        }
        if (subscription.stream == null) {
            Log.d(TAG, "subscription stream is null")
            return@launch
        }
        subscription.stream?.let { stream ->
            Log.d(TAG, "subscription finished. subscription id: ${subscription.id}, subscription stream id: ${stream.id}, steam type: ${stream.contentType}")
            val targetMember = members.find { it.id == publication.publisher?.id }
            targetMember?.let { groupMember ->
                if (stream.contentType == Stream.ContentType.VIDEO) {
                    withContext(Dispatchers.Main) {
                        groupMember.videoStream.value = subscription.stream as RemoteVideoStream
                    }
                }
                if (stream.contentType == Stream.ContentType.AUDIO) {
                    groupMember.audioStream.value = subscription.stream as RemoteAudioStream
                }
            }
        }
    }
}

退室

VideoChatViewModel.leaveChatRoom

LocalRoomMember.leaveを利用し、チャットRoomから退室します。

その後、 Room.disposeSkyWayContext.dispose によってリソースを解放します。

fun leaveChatRoom() {
    /**
     * [NOTICE]
     * SkyWayの退室処理は suspend 関数であり、完了までに時間がかかる可能性があります。
     * 一方で、ViewModelScope の終了とともに VideoModelScope もキャンセルされるため、
     * VideoModelScope 内で suspend 処理を実行すると、その処理が途中でキャンセルされてしまう場合があります。
     *
     * そのため、ViewModelScope よりも長く生存する CoroutineScope の利用が必要です。
     *
     * ここでは便宜上 GlobalScope を使用していますが、GlobalScope の利用は推奨されておらず、
     * 本番環境では適切なスコープ設計(例:アプリケーションスコープ等)を検討してください。
     */
    GlobalScope.launch {
        if (memberMe != null) {
            memberMe!!.leave()
        }
        members = emptyList()
        chatRoom?.dispose()
        SkyWayContext.dispose()
        Log.d(TAG, "leaveChatRoom succeed")
    }
}

以上で、ビデオ通話アプリの実装は完成です。

WebRTCを簡単に実装するには

WebRTCの実装には専門的な知識と多くの開発工数が必要ですが、SDK(Software Development Kit)を活用する方法があります。SDKを利用すれば、サーバーの構築や管理の手間を大幅に削減し、効率的な開発が可能になります。

WebRTC SDK「SkyWay」

SkyWay

WebRTCの代表的なSDKとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。 

「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。

大きな特徴としては、以下が挙げられます。

  • スピーディーな開発ができる
    開発資料が豊富かつ日本語でわかりやすい、国内エンジニアがサポートしてくれる
  • 信頼性・安全性が高い
    NTTコミュニケーションズが開発、運営する国産SDK。サービス歴は10年以上で、累計導入サービス数も21,000件以上
  • 無料で開発スタート
    開発検証用として、Freeプランあり。テスト検証期間中は無料で利用可能。商用サービス提供後も基本利用料11万 + 従量課金制で安心。

SkyWayの詳細はこちら

 

WebRTCのSDKとして提供されているものは、海外製が多いため、開発ドキュメントも英語か和訳のもので開発しにくい傾向にあります。 「SkyWay」であれば、NTTグループが開発、運営する安心の国産SDKかつ、国内エンジニアがサポートしてくれるため、開発運用工数も大幅に削減でき、開発のしやすさからもおすすめです。

テスト検証用は無料のため、ぜひアカウント登録をしてみください。

アカウント登録はこちら

まとめ

本記事では、WebRTCの基本から始まり、AndroidアプリにWebRTCを使ったビデオ通話機能を追加したい方向けに、JetChatサンプルをベースにSkyWay SDKを活用した実装方法をステップごとにご紹介しました。難しそうに思えるAndroidでのWebRTCの導入も、SkyWayを使えば複雑な設定なしでスムーズに構築できるので、非常におすすめです。

 

この記事を書いた人

下野 弘朗

NTTコミュニケーションズが開発、運営する「SkyWay」のソフトウェアエンジニア。
SkyWayのモバイル向けSDKの開発に従事。

この記事を書いた人

jin

NTTコミュニケーションズが開発、運営する「SkyWay」のソフトウェアエンジニア。
SkyWayのAndroid SDKの開発に従事。