A Compose Multiplatform wrapper around the native LiveKit client SDKs — one shared Kotlin API (rooms, participants, tracks, events) plus a common @Composable VideoRenderer, for Android and iOS.
- Android — backed by
client-sdk-android2.28.0(a normal Maven dependency). - iOS — backed by
client-sdk-swift2.16.0through a hand-written@objcSwift bridge (LiveKitKMPBridge), because the pure-Swift SDK isn't reachable from Kotlin/Native cinterop directly.
Status: Published and runnable on both platforms —
io.github.akardas16:compose-livekit:0.1.0on Maven Central (Android/KMP) and via SwiftPM (iOS). Android was tested live on two physical devices; iOS builds/links/launches and connected live to LiveKit Cloud from the simulator (iOS 26). Not yet done: a full-device iOS broadcast-extension screen share (iOS currently does in-app capture).
Latest version: 0.1.0
// build.gradle.kts
dependencies {
implementation("io.github.akardas16:compose-livekit:0.1.0")
}The transitive WebRTC dependency resolves via JitPack, so add it to your repositories:
// settings.gradle.kts → dependencyResolutionManagement.repositories
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }In Xcode: File → Add Package Dependencies… → https://github.com/akardas16/compose-livekit → add the ComposeLiveKit product. Or in Package.swift:
dependencies: [
.package(url: "https://github.com/akardas16/compose-livekit", from: "0.1.0"),
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "ComposeLiveKit", package: "compose-livekit"),
]),
]ComposeLiveKit bundles the LiveKitKMP XCFramework + the LiveKitKMPBridge bridge (which pulls in LiveKit + WebRTC). Link your app target with -ObjC, and set the Info.plist keys under iOS setup.
A minimal call screen in shared Compose code — connect, publish camera + mic, and render every participant's video:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.weight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.blindid.livekit.VideoRenderer
import com.blindid.livekit.rememberLiveKitRoom
@Composable
fun CallScreen(url: String, token: String) {
val room = rememberLiveKitRoom() // created now, closed when it leaves composition
val state by room.state.collectAsState()
val local by room.localParticipant.collectAsState()
val remotes by room.remoteParticipants.collectAsState()
LaunchedEffect(Unit) {
room.connect(url, token) // suspend
room.setCameraEnabled(true)
room.setMicrophoneEnabled(true)
}
Column(Modifier.fillMaxSize()) {
Text("State: $state")
// Your own camera preview (mirrored for the front camera)
VideoRenderer(
track = local?.videoTracks?.firstOrNull()?.videoTrack,
mirror = true,
modifier = Modifier.fillMaxWidth().weight(1f),
)
// Every remote participant's video
remotes.forEach { participant ->
VideoRenderer(
track = participant.videoTracks.firstOrNull()?.videoTrack,
modifier = Modifier.fillMaxWidth().weight(1f),
)
}
}
}Observe events and data messages with room.events (a SharedFlow<RoomEvent>), and send data with room.publishData(bytes). On Android, request the CAMERA and RECORD_AUDIO runtime permissions before enabling capture; on iOS add the usage keys (see iOS setup). A full example is in sample-shared.
| Capability | Android | iOS | Common API |
|---|---|---|---|
| Connect / disconnect / reconnect state | ✅ | ✅ | connect, disconnect, close, state |
| Camera enable + flip (front/back) | ✅ | ✅ | setCameraEnabled, switchCamera |
| Microphone enable | ✅ | ✅ | setMicrophoneEnabled |
| Video rendering (front-camera-only mirror) | ✅ | ✅ | @Composable VideoRenderer(track, mirror = …) |
| Participants (local + remote) & their data | ✅ | ✅ | localParticipant, remoteParticipants, activeSpeakers |
| Room events (local + remote) | ✅ | ✅ | events: SharedFlow<RoomEvent> |
| Data messages (send + receive) | ✅ | ✅ | publishData, RoomEvent.DataReceived |
| Audio output selection (speaker / earpiece / wired / Bluetooth) | ✅ | ✅¹ | audioDevices, selectedAudioDevice, selectAudioDevice |
| Screen share | ✅ | ✅² | setScreenShareEnabled |
¹ iOS routes via AVAudioSession (Speaker / receiver + connected accessories); best-effort.
² iOS uses in-app ReplayKit capture (shares the app's own content). Full-device capture needs a separate Broadcast Upload Extension (not included yet). Android uses MediaProjection + LiveKit's built-in foreground service.
| Path | What |
|---|---|
livekit/ |
The publishable Compose Multiplatform library. commonMain = shared API; androidMain = real livekit-android impl; iosMain = real impl via cinterop against the bridge. |
iosBridge/ |
LiveKitKMPBridge — a Swift Package exposing an @objc facade over the LiveKit Swift SDK (async→completion-handlers, structs/enums→ObjC-safe DTOs). |
sample-shared/ |
Shared Compose UI (App(), iOS MainViewController(), expect/actual ScreenShareButton) used by both sample apps. |
sample/ |
Android sample app (src/main) + the iOS Xcode app (iosApp/, generated with XcodeGen from project.yml). Both runnable. |
The library targets androidTarget, iosArm64, and iosSimulatorArm64 (Intel-simulator iosX64 is intentionally omitted).
Kotlin/Native cinterop only reads C/Objective-C headers, and LiveKit's Swift SDK is pure Swift. So the library binds a hand-written clean Objective-C header (livekit/src/nativeInterop/cinterop/include/LiveKitKMPBridge.h) that mirrors the bridge's @objc surface, and the real symbols + WebRTC link in the consuming app via the LiveKitKMPBridge SPM package. The KMP framework itself needs no Xcode/LiveKit/WebRTC to compile. See iosBridge/README.md.
val room = rememberLiveKitRoom() // Compose; or on Android: LiveKit.create(context)
room.connect(url, token) // suspend
room.setCameraEnabled(true); room.setMicrophoneEnabled(true)
room.switchCamera()
room.publishData("hi".encodeToByteArray(), topic = "chat")
// State & events (all Flows)
room.state // StateFlow<ConnectionState>
room.events // SharedFlow<RoomEvent> (Connected, ParticipantConnected, TrackSubscribed,
// TrackMuted, ActiveSpeakersChanged, ConnectionQualityChanged, DataReceived, …)
room.localParticipant // StateFlow<LocalParticipant?>
room.remoteParticipants // StateFlow<List<RemoteParticipant>>
room.activeSpeakers // StateFlow<List<Participant>>
// Participant data: identity, sid, name, metadata, isSpeaking, connectionQuality,
// videoTracks / audioTracks (TrackPublication: source, isMuted, isSubscribed, videoTrack/audioTrack)
// Audio output
room.audioDevices(); room.selectedAudioDevice(); room.selectAudioDevice(device)
// Video (common Composable; mirror is display-only)
VideoRenderer(track = participant.videoTracks.firstOrNull()?.videoTrack, mirror = false)
// Screen share (see platform notes below)
room.setScreenShareEnabled(true)- Kotlin 2.4.10, Compose Multiplatform 1.11.1, AGP 9.1.0, Gradle 9.6.1, JDK 11+.
- Android
minSdk 24. iOS deployment target 14+. iOS builds need Xcode 16+ and XcodeGen.
Add the JitPack repository — required, because LiveKit's WebRTC dependency resolves there:
// settings.gradle.kts → dependencyResolutionManagement.repositories
google(); mavenCentral()
maven { url = uri("https://jitpack.io") }Manifest permissions: INTERNET, CAMERA, RECORD_AUDIO, MODIFY_AUDIO_SETTINGS (request CAMERA / RECORD_AUDIO at runtime). For screen share also add FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PROJECTION and declare LiveKit's built-in service:
<service
android:name="io.livekit.android.room.track.screencapture.ScreenCaptureService"
android:exported="false"
android:foregroundServiceType="mediaProjection" />Then request the MediaProjection permission, hand the result to the room, and enable:
// after the MediaProjectionManager.createScreenCaptureIntent() result:
room.setScreenCaptureResult(resultIntent) // androidMain-only extension
room.setScreenShareEnabled(true)See sample/src/main/AndroidManifest.xml and sample-shared/src/androidMain/.../ScreenShare.android.kt for the full flow.
A consuming iOS app adds two things:
- The
compose-livekitKMP framework (produced by:sample-shared:embedAndSignAppleFrameworkForXcode, or an XCFramework of:livekit). - The
LiveKitKMPBridgeSwift package (iosBridge/) — which transitively pulls LiveKit + WebRTC and provides the Objective-C symbols.
Link with -ObjC (so the bridge's ObjC classes aren't dead-stripped), exclude x86_64 for the simulator SDK, and set CADisableMinimumFrameDurationOnPhone = true. Info.plist needs NSCameraUsageDescription + NSMicrophoneUsageDescription; enable the Audio, AirPlay, and Picture in Picture background mode. The sample/iosApp/project.yml shows a complete working setup.
Android (device/emulator connected):
./gradlew :sample:installDebug # or open in Android Studio and RuniOS:
brew install xcodegen # once
cd sample/iosApp && xcodegen generate && open iosApp.xcodeproj # then Run in XcodeEnter your LiveKit server URL (wss://…) and an access token, tap Connect. The sample exercises the full API: camera/mic, flip, screen share, data messages, an audio-output picker, a live event log, and a participants panel — plus a video grid with a speaking indicator.
./gradlew :sample:assembleDebug # Android app (compiles :livekit + :sample-shared)
./gradlew :livekit:compileKotlinIosSimulatorArm64 # iOS library
./gradlew :livekit:linkDebugFrameworkIosSimulatorArm64 # iOS framework links
cd iosBridge && xcodebuild -scheme LiveKitKMPBridge -destination 'generic/platform=iOS Simulator' buildNote: the library uses AGP 9's
androidLibraryKMP DSL (single variant), so there is no:livekit:assembleDebug— verify the library by building the sample.
Apache License 2.0 — see LICENSE.