Android SDK (Kotlin) โ
com.rtcstack:sdk is the Kotlin SDK โ a thin, idiomatic wrapper over LiveKit's livekit-android, mirroring the shipped @rtcstack/sdk Call API with StateFlow / SharedFlow reactivity. com.rtcstack:ui-compose adds a drop-in Jetpack Compose conference UI built on livekit-android-compose-components.
- Min target: API 24 (Android 7.0)+,
compileSdk34, JDK 17, Kotlin 2.0 - Pinned deps:
livekit-android2.24.1,livekit-android-compose-components2.3.0 setCommunicationDevicerouting needs API 31+; typed foreground services need API 29+/34 rules
Preview โ not yet on Maven Central
The AARs are not yet published. Build from source against the committed Gradle wrapper today; Maven coordinates land on first publish. Device features (foreground service, FCM incoming call, MediaProjection) need a real-device pass with your own Firebase credentials.
Install coordinates on publish โ
Once published:
// build.gradle.kts (module)
dependencies {
implementation("com.rtcstack:sdk:1.0.0")
implementation("com.rtcstack:ui-compose:1.0.0") // optional Compose UI kit
}The SDK declares livekit-android as an api dependency, ships consumer ProGuard rules (so LiveKit/WebRTC native classes survive R8 automatically), and declares the ongoing-call foreground service in its manifest โ the host app inherits both.
Building from source: clone radioBros/RTCstack and ./gradlew :sdk:assembleRelease :ui-compose:assembleRelease.
Quick start โ
import com.rtcstack.sdk.CallOptions
import com.rtcstack.sdk.RTCstack
import com.rtcstack.sdk.call.RTCstackCallService
// token + url come from YOUR backend (POST /v1/token) โ never embed API secrets.
val call = RTCstack.createCall(this, CallOptions(token = jwt, url = wssUrl))
RTCstackCallService.start(this) // ongoing-call foreground service (keeps mic/cam alive)
lifecycleScope.launch {
call.connect()
call.setMicEnabled(true)
call.setCameraEnabled(true)
}The drop-in Compose UI:
import com.rtcstack.ui.RTCstackTheme
import com.rtcstack.ui.VideoConference
setContent {
RTCstackTheme {
VideoConference(call = call, onLeave = {
RTCstackCallService.stop(this)
call.release()
})
}
}VideoConference collects the Call's StateFlows and renders the video grid + control bar, handling connecting / disconnected states.
The Call API โ
Create with RTCstack.createCall(context, options); it does not connect until you call connect(). Call release() when done to free the room + coroutine scope.
val call = RTCstack.createCall(
context,
CallOptions(
token = jwt,
url = wssUrl,
tokenRefresher = { myBackend.freshToken() }, // optional, calls YOUR backend
),
)Connection โ
call.connect() // suspend; mints/refreshes token if near expiry, then connects
call.disconnect() // suspend; idempotent
call.release() // free the room when the call object is no longer needed
call.connectionState // StateFlow<ConnectionState>: IDLE | CONNECTING | CONNECTED | RECONNECTING | DISCONNECTED
call.tokenExpiresAt // epoch millis, decoded from the JWTMedia control โ
call.toggleMic()
call.setMicEnabled(true)
call.toggleCamera()
call.setCameraEnabled(true)
call.stopScreenShare()
// startScreenShare needs the MediaProjection consent Intent โ use ScreenShareLauncher belowMessaging โ
call.sendMessage("hi")
call.sendMessage("psst", to = listOf("alice")) // direct message
call.sendReaction("๐")LiveKit does not echo your own sent data back; ui-compose renders your outgoing messages/reactions locally.
Reactive state (StateFlow) โ
call.participants // StateFlow<Map<String, Participant>>
call.localParticipant // StateFlow<Participant?>
call.activeSpeakers // StateFlow<List<Participant>>
call.messages // StateFlow<List<Message>>
call.layout // StateFlow<Layout> โ GRID | SPEAKER | SPOTLIGHT
call.pinnedParticipant // StateFlow<String?>
// In Compose:
val participants by call.participants.collectAsStateWithLifecycle()Discrete events (SharedFlow) โ
lifecycleScope.launch {
call.events.collect { event ->
when (event) {
is CallEvent.ParticipantJoined -> log(event.participant.name)
is CallEvent.MessageReceived -> appendChat(event.message)
is CallEvent.TranscriptReceived -> appendTranscript(event.segment)
is CallEvent.ScreenShareStarted -> showScreenTile(event.participant)
is CallEvent.Reconnecting -> showBanner(event.attempt)
is CallEvent.Error -> handle(event.code, event.message)
else -> Unit
}
}
}CallEvent is a sealed interface mirroring the web SDK's CallEventMap, dropping the web-only audioPlaybackBlocked and adding CallSuspended / CallResumed (app background/foreground) and PermissionDenied(kind).
Native call stack โ
Foreground service & audio focus โ
RTCstackCallService is the ongoing-call foreground service โ required to keep mic/camera (and screen share) alive while backgrounded; Android kills background mic/camera access otherwise. It declares the typed FGS (microphone|camera|mediaProjection) and is promoted to include mediaProjection when screen share starts.
RTCstackCallService.start(context) // after connect
RTCstackCallService.start(context, withScreenShare = true) // promote for screen share
RTCstackCallService.stop(context) // on leaveThe SDK also requests audio focus and MODE_IN_COMMUNICATION, using setCommunicationDevice on API 31+ (falling back to setSpeakerphoneOn / Bluetooth SCO below 31). Audio-focus loss (an incoming GSM call) emits CallSuspended.
Incoming calls (FCM + notification) โ
The recommended baseline for incoming calls from a backgrounded/killed app is a high-priority FCM data message โ foreground service โ full-screen incoming-call notification. IncomingCallManager builds the notification (full-screen intent, call category):
IncomingCallManager(context).showIncomingCall(
callId = 1,
callerName = "Alice",
fullScreenIntent = incomingCallActivityIntent,
acceptIntent = acceptIntent,
declineIntent = declineIntent,
)
// On accept: mint a token via your backend, then RTCstack.createCall(...).connect()The SDK does not depend on firebase-messaging โ your app's FirebaseMessagingService extracts the payload and calls in. A reference RTCstackMessagingService + IncomingCallActivity live in the in-repo android/incoming-call-example/. Self-managed Telecom ConnectionService is an advanced opt-in.
Screen share (MediaProjection) โ
ScreenShareLauncher wraps the consent dialog + foreground-service promotion + LiveKit track into one call. Register it in your Activity onCreate (it uses the Activity Result API):
private lateinit var screenShare: ScreenShareLauncher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
screenShare = ScreenShareLauncher(
context = this, caller = this, call = call, scope = lifecycleScope,
onDenied = { /* SCREEN_SHARE_DENIED โ user cancelled consent */ },
)
}
// later, from a click handler:
screenShare.launch() // shows the system consent dialog, then starts sharing
screenShare.stop() // stops + demotes the FGS back to mic/cameraWhat your app must provide โ
| Why | |
|---|---|
Firebase/FCM project + google-services.json | High-priority incoming-call push |
Runtime permission flow (RECORD_AUDIO, CAMERA, POST_NOTIFICATIONS) | Android 6+/13+ |
Token-minting backend (POST /v1/token) | Secrets never ship in the binary |
| Real-device QA | FGS, MediaProjection, audio focus can't be validated on emulators |
Manifest & permissions โ
The SDK's manifest already declares the foreground service and these permissions (merged into your app):
INTERNET ยท ACCESS_NETWORK_STATE ยท RECORD_AUDIO ยท CAMERA ยท MODIFY_AUDIO_SETTINGS ยท
BLUETOOTH_CONNECT ยท FOREGROUND_SERVICE (+ _MICROPHONE / _CAMERA / _MEDIA_PROJECTION) ยท
POST_NOTIFICATIONS ยท USE_FULL_SCREEN_INTENTRequest the dangerous ones (RECORD_AUDIO, CAMERA, POST_NOTIFICATIONS) at runtime before connecting. Android 14+ restricts full-screen intents to calling/alarm apps.
UI components โ
| Component | Description |
|---|---|
VideoConference() | Full drop-in conference (grid + control bar) |
VideoGrid() | Participant video grid with layout switching |
ParticipantVideo() | Single participant tile |
ControlBar() | Mic / camera / screen / reactions / layout / leave |
ChatPanel() | Scrolling chat with input |
Wrap your UI in RTCstackTheme { โฆ } to apply the design tokens (Material 3, light/dark parity with the web kit). See Theming for the shared token set.
Next steps โ
- iOS SDK โ
- Token Flow โ minting tokens on your backend

