Skip to content

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)+, compileSdk 34, JDK 17, Kotlin 2.0
  • Pinned deps: livekit-android 2.24.1, livekit-android-compose-components 2.3.0
  • setCommunicationDevice routing 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:

kotlin
// 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 โ€‹

kotlin
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:

kotlin
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.

kotlin
val call = RTCstack.createCall(
    context,
    CallOptions(
        token = jwt,
        url = wssUrl,
        tokenRefresher = { myBackend.freshToken() },  // optional, calls YOUR backend
    ),
)

Connection โ€‹

kotlin
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 JWT

Media control โ€‹

kotlin
call.toggleMic()
call.setMicEnabled(true)
call.toggleCamera()
call.setCameraEnabled(true)
call.stopScreenShare()
// startScreenShare needs the MediaProjection consent Intent โ€” use ScreenShareLauncher below

Messaging โ€‹

kotlin
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) โ€‹

kotlin
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) โ€‹

kotlin
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.

kotlin
RTCstackCallService.start(context)              // after connect
RTCstackCallService.start(context, withScreenShare = true)  // promote for screen share
RTCstackCallService.stop(context)               // on leave

The 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):

kotlin
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):

kotlin
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/camera

What your app must provide โ€‹

Why
Firebase/FCM project + google-services.jsonHigh-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 QAFGS, 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_INTENT

Request the dangerous ones (RECORD_AUDIO, CAMERA, POST_NOTIFICATIONS) at runtime before connecting. Android 14+ restricts full-screen intents to calling/alarm apps.

UI components โ€‹

ComponentDescription
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 โ€‹