0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin Multiplatform】 WebRTC / WebSocket アプリ開発が想像以上に簡単だった!

Last updated at Posted at 2025-09-23

「Kotlin Multiplatform (KMP) に興味はあるけど、まだ使ったことがない」

…という Android 開発者の方に向けて、この記事では、WebRTCとWebSocketを使ったリアルタイム通信アプリを、意外と簡単に開発できた経験を共有します。

はじめに

普段の私 @isseikz は、主にAndroid アプリ開発をしています。

Android だけでなく、iOS や Web に開発範囲を広げたいと考えていますが、プラットフォーム別の技術習得コストを課題に感じていました。その解決策として、Kotlin Multiplatform (KMP) と Compose Multiplatform (CMP) に興味がありました。

このたび、新たに作りたいものが浮かんできたので、ここらで KMP/CMP を使ったシステムを構築し、その使用感をレビューしたいと思います (← セリフが YouTuber くさい)。

それではさっそくやっていきましょう。

お題 - Android TV リモートリモコン

Android TV のミラーリングとリモコン操作を、PCやスマホから遠隔で行える iOS・Android・Web アプリを作りました。

ポイントは、キャプチャ映像とリモコン操作が一つの画面に統合されていることです。こういうアプリがあまりないので困っていました。

(左から iOS, Android, Google Chrome)

リポジトリはこちら (Pull Request 歓迎です!) : isseikz/ATV-Remote

よかったところ

① プロジェクトのセットアップがとにかく簡単

JetBrains が提供する公式ウィザード を利用すれば、 Hello World アプリをビルドでき、特につまづくこともありませんでした。

開発を進める中で、iOS のライブラリ導入など、後述するハマりどことはありましたが、まずは Android と共通部分の開発に集中できたので、開発モチベを維持しやすかったです。

② Android開発のスキルが想像以上に活かせる

Androidx の API は使えないだろうな…。と想像していましたが、ViewModelLifecycle など、普段使っているAndroidx 系のライブラリがそのまま使えました。MVVM パターンに慣れている人なら、すぐに安定動作するアプリが作れるはずです。

③ Ktor と Kotlin RPC の統合が直感的

この二つを組み合わせることで、サーバー側とクライアント側の API 仕様の実装を、共通モジュールの interface で統一できました。
通信していることをほぼ意識せずに、型安全な RPC を実装できることが、とても面白かったです。

ハマったところと解決策

iOS ビルドのライブラリ導入

前置きとして、iOS をはじめとした XCode 開発では、CocoaPods 等を用いてライブラリ管理するそうです。

JetBrains が提供する CocoaPodsと Gradleの連携プラグイン は、Kotlin 2.0 から仕様が変わった様で、情報検索に苦労しました。

※最終的な記述方法は補足欄に記載しました。

サーバー側 WebRTC ライブラリの組み込み

Apple Silicon 環境では、webrtc-java ライブラリが実行時エラーになり、WebRTC API にアクセスできない問題がありました。

build.gradle.kts でプラットフォームを明示する必要がありました。

server/build.gradle.kts
dependencies {
    runtimeOnly(libs.webrtc.java) {
        artifact {
            classifier = "macos-aarch64"
        }
    }

    (略)

WebRTC の振る舞いの一貫性

WebRTC の動作が、Web ブラウザによって動作がまちまちの様です。
Android の Chrome では問題なく接続できましたが、iOS の Safari/Chrome ではうまく動作していません。ICE Candidate の送信まわりに問題だと推測しますが、未解決問題の課題です。

総評

Android 開発者にとって、とにかく簡単に iOS/Web アプリを作ることができました。開発初期のハードルが劇的に下がったことは率直に感動的でした。

もちろん、プラットフォーム固有の実装が少なからず残り、解決にはそれなりの知識/LLM が必要になりますが、共通化によって生まれた時間とパワーを、各プラットフォームの差分の理解に充てられることは、大きなメリットだと思いました。

個人的には、今後も使い続けたいと思います。

補足

システムの全体像

設計コンセプト

  • 制御関係: WebSocket で通信することで、信頼性の高いデータ授受を実現する
  • 映像配信機能: WebRTC で低遅延な配信を実現する
  • 映像取得: Android デバイスの映像を HDMI キャプチャで取得することで、OS 標準機能で映像を取り込む

システム構成 - 2025/09/22

技術スタック

カテゴリ 技術 バージョン 役割
言語 Kotlin 2.2.0 プロジェクト全体の開発言語
プラットフォーム Kotlin Multiplatform (KMP) 2.2.0 マルチプラットフォーム開発基盤
UIフレームワーク Compose Multiplatform 1.8.2 宣言的クロスプラットフォームUI
サーバー Ktor 3.2.1 WebSocketサーバー、HTTP API
RPC kotlinx-rpc 0.9.1 型安全なRPC通信フレームワーク
WebRTC (共通) webrtc-kmp 0.125.11 マルチプラットフォームWebRTC実装
WebRTC (Server) webrtc-java 0.14.0 サーバーサイドWebRTC、MediaDevices映像キャプチャ機能含む
WebRTC (iOS) WebRTC-SDK 125.6422.07 iOS向けネイティブWebRTC実装
シリアライゼーション kotlinx-serialization 1.8.0 JSON シリアライゼーション
コルーチン kotlinx-coroutines 1.10.2 非同期処理・並行処理

Cloudflare Realtime TURN サーバー

NAT越えのために Cloudflare が提供する TURN サーバー を利用しました。 個人用途としては意外と安価で、利用方法も簡単だったことが選定理由です。

参考に価格設定を掲載します。

Freeプラン 有料プラン
クライアントへのデータ転送 1,000GB/月 1,000GB/月まで無料、以降従量制$0.05/GB
クライアントからのデータ転送 無料 無料

引用元: SFUおよびTURNサーバーの価格設定 - Cloudflare Realtime

Ktor と Kotlin RPC によるシグナリング機能の実装

Ktor のプラグインとして WebSocket と Kotlin RPC をインストールすることで、WebSocket の通信実装が自動生成されます。エンドポイントの実装も、共有モジュールで定義した interface を登録するだけでした。

Routing.kt
fun Application.configureRouting(
    signaling: ISignalingService,
    sessionManager: SessionManager
) {
    routing {
        // RPC WebSocket endpoint for kotlinx-rpc communication
        rpc(PATH_RPC) {
            rpcConfig {
                serialization {
                    json()
                }
            }

            // ここに RPC 仕様を interface と具象クラスで定義する
            registerService<IAtvControlService> { AtvControlServiceImpl(this@configureRouting, sessionManager) } // TODO セッション別に別の adb 接続を管理する
            registerService<ISignalingService> { signaling }
            registerService<ISessionService> { SessionServiceImpl(sessionManager) }
        }
    }
}
RPC.kt
fun Application.configureRPC() {
    install(WebSockets) {
        pingPeriod = 15.seconds
        timeout = 5.seconds
        maxFrameSize = Long.MAX_VALUE
        masking = false
    }

    install(Krpc)
}
ISignalingService.kt

@Rpc
interface ISignalingService {

    /**
     * クライアントA が ウェイティングリストに自身を登録して、Offer を待機する
     *
     * @param request セッションの詳細情報
     * @return SDP Offer
     */
    fun waitForOffer(request: SessionRequest): Flow<SignalingOffer>

    /**
     * クライアントB が待機中のクライアントA に Offer を送信し、Answer を待ち受ける
     * @param offer クライアントから送信されたSDP Offer
     * @return 対応するSDP AnswerとICE Candidate
     */
    fun offer(offer: SignalingOffer): Flow<SignalingAnswer>

    /**
     * クライアントA がクライアントB に Answer を送信し、Candidate を待ち受ける
     *
     * @param answer SDP Answer
     * @return リモートデバイスの ICE Candidate. これを setRemoteDescription 後に addIceCandidate で追加する
     */
    fun answer(answer: SignalingAnswer): Flow<SignalingCandidate>

    /**
     * 自身の通信経路が変わったときに、相手に新しい Candidate を送信する
     * @param candidate クライアントから送信されたICE Candidate情報
     */
    fun putIceCandidates(candidate: SignalingCandidate): Flow<Unit>
}

Kotlin CocoaPods プラグインの宣言方法

iOSビルドが成功した設定ファイルを共有します。ios.deploymentTargetとpodfileのパス指定がポイントでした。

※ Kotlin 2.2.0 でのみ動作確認したため、他のバージョンではうまく動作しない可能性があります。

composeApp/build.gradle.kts
kotlin {
    androidTarget {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_18)
        }
    }

    cocoapods {
        version = "1.0"
        summary = "Some description for a Kotlin/Native module"
        homepage = "Not yet"
        ios.deploymentTarget = "18.0" // ポイント1 これを XCode のプロジェクト設定と合わせておかないと、CocoaPods の target version 不一致でビルドが失敗する

        framework {
            baseName = "ComposeApp"
            isStatic = false
        }

        pod("WebRTC-SDK") {
            version = libs.versions.webrtc.ios.get()
            moduleName = "WebRTC"
            packageName = "WebRTC"
            linkOnly = true
        }

        podfile = project.file("../iosApp/Podfile") // ポイント2 composeApp のパスを手動登録しないと、iOS ビルド時に Compose のモジュールを解決できない

        xcodeConfigurationToNativeBuildType["CUSTOM_DEBUG"] = NativeBuildType.DEBUG
        xcodeConfigurationToNativeBuildType["CUSTOM_RELEASE"] = NativeBuildType.RELEASE
    }


    iosArm64()
    iosSimulatorArm64()

    (略)
# iosApp/Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'

target 'iosApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iosApp
  pod 'composeApp', :path => '../composeApp'
end

最後まで読んでいただき、ありがとうございました!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?