Skip to content

SDKs

Android SDK (Kotlin) for rooms, chat and broadcast

The VoltKit RTC Kotlin SDK: join a room from an Android app, publish camera and microphone, add chat, go live over WHIP and put in-app music on air.

in.voltkit:voltkit-rtc is the Android SDK for VoltKit RTC. It uses the same names as the JavaScript, Flutter and iOS SDKs: StreamClient for rooms, ChatClient for chat only, and BroadcastPublisher for going live. It is written in Kotlin, with coroutines and Flow throughout.

Install

// settings.gradle.kts: mavenCentral() must be among the repositories.

// app/build.gradle.kts
dependencies {
    implementation("in.voltkit:voltkit-rtc:0.2.0")
}

That one line brings everything: the protocol layer, the mediasoup client, and the WebRTC build it contains.

RequirementValue
minSdk23 (Android 6.0)
compileSdk36 or newer
Java/Kotlin target11 or newer
ABIsarm64-v8a, armeabi-v7a, x86_64, with 16 KB page alignment as Google Play requires
LicenceMIT

Do not add another WebRTC artifact to the same app. Two copies of libwebrtc in one APK clash at load time.

Permissions

Publishing needs camera and microphone permission, declared in the manifest and granted at run time before publish(). A viewer-only app needs neither.

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

If a permission is missing, publish() throws RpcError("permission-denied") instead of sending a black or silent track. To keep a live session going in the background on Android 14+, run a foreground service of type camera|microphone.

Join a room

Your backend mints a token and hands the app token and wsUrl (see Authentication). The API secret never reaches the device.

import com.voltkit.rtc.*

val client = StreamClient(context, wsUrl, token)

// Collect before connecting: tracks already live arrive during connect().
lifecycleScope.launch {
    client.onTrack.collect { track ->
        if (track.isVideo) track.addSink(remoteRenderer)   // audio plays by itself
    }
}
lifecycleScope.launch { client.onClosed.collect { reason -> finish() } }

lifecycleScope.launch {
    val join = client.connect()
    if (join.role.canPublish) client.publish()            // camera + mic, simulcast
}

override fun onDestroy() {
    client.close()       // always, when leaving the screen
    super.onDestroy()
}

Video quality. 1080p, 720p and 480p are available on every project; video up to 1080p at 30 fps is billed at one rate (see Usage and billing). The preview native SDK still uses its documented capture and encoding defaults; it has not gained the browser/Flutter 0.5.0 preset, frame-rate or screen-share APIs. Set native capture/encoding explicitly if changing quality. The service allows up to 1080p and 60 fps by default, with a shared 25 Mbps incoming safety cap per publisher; explicit operator restrictions still apply. See Video quality.

Renderers share the SDK's EGL context:

val renderer = SurfaceViewRenderer(context)
renderer.init(VoltKitRtc.eglBaseContext, null)
client.localVideoTrack.collect { track -> track?.addSink(localRenderer) }

Every suspend function is main-safe. setMicEnabled, setCamEnabled and switchCamera() change what you send without republishing.

Events

Each event is a Flow on the client. Collect it in a coroutine.

FlowWhen
onTrack / onTrackEnded / onTrackMutedA track you receive started, stopped, or was paused
onParticipantJoined / onParticipantLeft / onParticipantUpdatedSomeone joined, left, or changed their metadata
onRoleChangedSomeone was promoted or demoted. A demoted publisher stops publishing
onHandRaised / onForceMutedHands and moderator mutes
onClaimChanged / onRoomMetadataChangedSlots and room metadata changed
onMessage / onMessageDeleted / onChatMuted / onTypingChat
onUsageLimitThe account ran out of minutes or hit a usage limit. See below
onReconnectedBack after a network change
onClosedThe session ended for good: replaced, kicked, roomClosed, gaveUp or left

Chat

Every room has a chat channel of the same name on the same connection:

client.onMessage.collect { m -> adapter.add(m) }                 // yours included
client.sendMessage("hello everyone", clientMsgId = "c-1")
client.sendMessage("psst", to = "user-42")                       // direct message
val older = client.getMessages(limit = 50, before = oldest.id)

For an audience outside the room, such as a broadcast's viewers, use ChatClient with a chat token. Its wsUrl ends in /ws/chat.

val chat = ChatClient(wsUrl, token)
lifecycleScope.launch { chat.onMessage.collect(::addMessage) }
val joined = chat.connect()          // joined.messages: the latest 50
chat.sendMessage("first!")

Show text in a TextView, never through Html.fromHtml or a WebView.

Go live (broadcast)

Your backend creates a stream with POST /v1/streams and gives the app publish.whipUrl. Treat it like a password. Viewers play the LL-HLS URL with ExoPlayer/Media3, so they need nothing from this SDK.

val publisher = BroadcastPublisher(context, whipUrl)
lifecycleScope.launch { publisher.onState.collect { showState(it) } }  // connecting, live, interrupted, failed, stopped
publisher.start()
publisher.setMicEnabled(false)
publisher.stop()

Broadcast video is H.264 only, so the media server can repackage it without transcoding. If a device has no H.264 encoder, it still publishes and onWarning says so. When the server refuses, start() throws WhipError: isRefused means the URL is expired or for another stream.

In-app music

A host can play songs from inside the app and put them on air without the speaker, with the music and the microphone on separate switches. See In-app music for the concept.

val player = MusicPlayer(context)
player.play(songUri)                 // MP3, AAC, Ogg/Opus, FLAC, WAV
client.publishMusic(player)          // or publisher.setMusic(player) on a broadcast

client.setMusicEnabled(false)        // music off for the audience, voice stays
client.setMicEnabled(false)          // voice off, music keeps playing
client.setMusicVolume(0.5f)          // the audience's music level
player.monitorVolume = 0.3f          // what the host hears; 0 = audience only
client.stopMusic()

If your app already decodes audio, push the PCM to PcmMusicSource instead. On Android the music is mixed into the microphone track before encoding, so viewers receive one track carrying both voice and music. A web host's separate music track does arrive on Android with isMusic set, and a viewer can turn it down with setVolume. Music needs RECORD_AUDIO even with the microphone off.

Usage limits

When the account runs out of minutes or reaches a daily or monthly limit, live sessions are warned first:

client.onUsageLimit.collect { limit ->
    showBanner("This stream ends in ${limit.graceSeconds} seconds")
}

Unless the account is topped up within the grace period, the server ends the session with WebSocket close code 4003. onClosed reports roomClosed, and the SDK does not reconnect. See Usage and billing.

Errors

Calls throw RpcError (code, message, data, isTerminal). WHIP throws WhipError (status, isRefused). Act on the code:

try {
    client.claim("seat-3")
} catch (e: RpcError) {
    if (e.code == "slot-taken") showTaken() else throw e
}

The server's codes are listed in Errors. The SDK adds permission-denied, disconnected, timeout and music-unsupported. The consumer R8 rules ship with the library, so no ProGuard setup is needed.

Help

Email contact@voltkit.in with your SDK version, Android version and device model.

Last updated . Something unclear or wrong? Tell us.