🚧 Kotlin Multiplatform(KMP)でVJソフトを開発しようとして失敗した話
📌 はじめに
映像と音楽を同期させてリアルタイムで操作できる VJソフト を Kotlin Multiplatform(KMP) を使って開発しようとしました。しかし、プラットフォームごとの制約やライブラリの互換性 などの問題に直面し、最終的には開発を断念することになりました。
本記事では、どのような構成を目指し、どのような課題に直面し、どう解決しようとしたか を詳しく解説します。
🎯 なぜKMPを使おうとしたのか?
VJソフトは音楽と映像をリアルタイムで同期させるアプリのため、Android/iOS/Desktopで共通のロジックを持たせる ことが理想でした。KMPを採用することで、以下のメリットを期待しました。
✅ 期待したメリット
- ビジネスロジック(音楽のBPM解析、映像シンクロ)を共通化
- UIは各プラットフォームで独自実装するが、データ処理は統一
- 1つのコードベースで管理し、メンテナンス性向上
しかし、実際には以下の課題に直面し、KMPを適用するのが困難になりました。
🎯 VJソフトに必要な機能とKMPの課題
🔹 必須機能
- 音楽・映像の再生・停止・シーク
- 音楽のBPMに合わせた映像エフェクトの適用
- 複数の映像・音声の同期とミックス
🔸 KMPでの課題
- プラットフォームごとにメディア再生APIが異なり、統一が難しい
- メタデータ取得方法がOSごとに異なる
- JavaFXがAndroid/iOSと競合し、KMPの統合が困難
- 依存関係の整理が難しく、Clean Architectureを維持しにくい
❌ 躓いたポイント
① KMPで音楽・映像の再生を統一するのが難しい
発生した問題
- Android → ExoPlayerはシークやピッチ調整のカスタマイズが必要
- iOS → AVFoundationはObjective-C/Swiftの依存が強く、KMPでの連携が難しい
- Desktop → JavaFX Media APIはリアルタイム処理に向かない
- FFmpeg/GStreamer → KMPの公式ラッパーが未成熟で、各OSごとにNativeライブラリが必要
➡ 結果:KMPでの完全統一を断念し、各プラットフォームごとの実装に切り替え
📦 project-root
┣ 📂 composeApp # アプリのエントリポイント(UIの管理)
┃ ┣ 📂 commonMain # アプリの初期化処理
┃ ┣ 📂 androidMain # Android用のUIと実装
┃ ┣ 📂 iosMain # iOS用のUIと実装(後から対応予定)
┃ ┗ 📂 desktopMain # Desktop用のUIと実装(JavaFXを使用)
┣ 📂 sharedModule # KMPで共通化するモジュール
┃ ┣ 📂 data # データの管理(ファイルの読み込み、メタデータ取得)
┃ ┣ 📂 domain # ビジネスロジック(UseCaseなど)
┃ ┣ 📂 di # 依存関係の管理
┃ ┗ 📂 presenter # ViewModelの管理
┃ ┃ ┣ 📂 commonMain # Android & iOSで共通のViewModel
┃ ┃ ┣ 📂 androidMain # Android固有のViewModel
┃ ┃ ┣ 📂 iosMain # iOS固有のViewModel
┃ ┃ ┗ 📂 desktopMain # Desktop固有のViewModel
┣ 📂 data_desktop # Desktop向けのファイル処理(JavaFXを利用)
ポイント
- Android/iOS は 共通UI(Jetpack Compose Multiplatform) を採用。
- Desktop(JavaFX) は 別モジュール(data_desktop) に分離。
- ビジネスロジック(domain) は OS非依存 にする設計。
- データ管理(data) は OSごとの違いに対応するため、ファイル取得やメタデータ取得をプラットフォーム別に実装。
- DI(di) は 依存関係を管理し、将来的に Desktop の依存関係を切り分けられるよう設計。
👉 この構成により、Android/iOS で共通の UI を使いながら、Desktop では JavaFX を使う柔軟な設計を目指しました。
② JavaFXがAndroid/iOSと競合する
発生した問題
Unresolved reference: javafx
The 'java' plugin has been applied, but it is not compatible with the Android plugins.
試したこと
- ✅ gradle.properties にJavaFXの設定 → Gradle Daemon起動エラー
- ✅
compose.desktop.applicationDefaultJvmArgs
に--module-path
を設定 → プラグイン競合エラー - ✅ Desktop専用の
data_desktop
モジュールを作成 → KMPでの統一は断念
➡ 結果:Android/iOSとDesktopを同じサブモジュールで扱うのを断念し、Desktop専用モジュールを作成。
③ KMPの依存関係の整理が難しい
発生した問題
- data モジュールが domain のクラスを参照すると依存関係が逆転
- repository を data に置くと domain に依存し、クリーンアーキテクチャが崩れる
具体的な影響
- 音楽ファイルの取得ロジックが複雑化
- BPM解析や映像のシーク処理をdomain層で統一できなくなった
- 結果としてKMPのメリット(共通化)が減少
解決策
✅ DTOを導入して、data層がdomainに依存しない設計に変更
✅ Repositoryのinterfaceをdomainではなくdata側に配置
// data 層の DTO
data class MediaFileDto(val uri: String, val title: String, val duration: Long)
// domain 層のモデル
data class MediaFile(val uri: String, val title: String, val duration: Long)
// data 層の Repository
interface MediaRepository {
fun getMediaFile(uri: String): MediaFileDto
}
class MediaRepositoryImpl(private val dataSource: MediaFileDataSource) : MediaRepository {
override fun getMediaFile(uri: String): MediaFileDto {
return dataSource.loadMediaFile(uri)
}
}
但し上述の解決策には下記のデメリットも存在します。
・リアルタイム性が求められるVJソフト では、音楽のBPMと映像を同期させるために、フレーム単位でデータ変換が必要です
・音楽メタデータの取得・変換 も、プラットフォームごとに異なる処理が走るため、毎回DTOを経由するのが負担になります
・こうした要素が積み重なり、パフォーマンスのボトルネックになる可能性があります
結論
・KMPでVJソフトを開発しようとしたが、現時点では難易度が高い
・音楽・映像の再生はプラットフォームごとに対応する必要がある
・JavaFXの統合が困難で、DesktopはKMPを諦める選択に
・依存関係の整理が難しく、開発コストが増加
👉 KMPは便利だが、VJソフトのようなマルチメディアアプリには課題が多いと実感した。
🔍 次回予告:低レイヤーまで考慮したマルチプラットフォーム開発の課題
今回の記事では、KMPを活用したVJソフト開発の課題について解説しました。しかし、そもそも ソフトウェアが実際にハードウェア上でどのように動作するのか を理解することが、より適切な開発手法を選択する上で重要です。
次回は、ソフトウェア開発者が実装したコードがAPI LayerからSystem Library、Kernelを経てハードウェアに実行される仕組み を踏まえ、マルチプラットフォーム開発におけるアプローチを探ります。