Kotlin Multiplatform¶
There is no Kotlin Multiplatform media SDK, and the community wrappers aren't production-ready. The recommended structure:
flowchart TB
subgraph common [commonMain]
API[Tele API client · Ktor]
SM[Call / invite state machine]
CE[[CallEngine interface]]
end
subgraph android [androidMain]
AE[AndroidCallEngine<br/>io.livekit:livekit-android]
end
subgraph ios [iOS app target · Swift]
IE[SwiftCallEngine<br/>client-sdk-swift via SPM]
end
SM --> CE
AE -. implements .-> CE
IE -. implements, injected into Kotlin .-> CE
- Shared code handles everything Tele-specific: tokens, preview, join, errors, chat models.
- Platform code handles media and UI: Compose on Android, SwiftUI on iOS.
- Don't use cinterop to call the Swift SDK. Its async APIs aren't visible to Kotlin/Native. Implement
CallEnginein Swift and inject it.
Shared: join¶
@Serializable data class IceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@Serializable data class JoinResponse(
@SerialName("ws_url") val wsUrl: String,
val token: String,
@SerialName("ice_servers") val iceServers: List<IceServer>? = null,
val identity: String,
val role: String,
)
interface CallEngine {
suspend fun connect(wsUrl: String, token: String, iceServers: List<IceServer>?)
suspend fun setMicrophone(enabled: Boolean)
suspend fun setCamera(enabled: Boolean)
suspend fun sendChat(text: String)
fun disconnect()
}
class TeleApi(private val http: HttpClient, private val baseUrl: String, private val tokenProvider: suspend () -> String) {
suspend fun joinByInvite(code: String, platform: String): JoinResponse {
val res = http.post("$baseUrl/v1/invites/${code.encodeURLPathPart()}/join") {
bearerAuth(tokenProvider())
header("X-Client-Platform", platform)
}
if (!res.status.isSuccess()) throw TeleException(res.body<ErrorEnvelope>().error)
return res.body()
}
}
Android engine¶
class AndroidCallEngine(context: Context) : CallEngine {
private val room = LiveKit.create(context)
override suspend fun connect(wsUrl: String, token: String, iceServers: List<IceServer>?) {
val options = if (iceServers == null) ConnectOptions() else ConnectOptions(
// rtcConfig MUST be set, otherwise the SDK ignores custom ICE servers.
rtcConfig = PeerConnection.RTCConfiguration(iceServers.map { s ->
PeerConnection.IceServer.builder(s.urls)
.setUsername(s.username ?: "")
.setPassword(s.credential ?: "")
.createIceServer()
})
)
room.connect(wsUrl, token, options)
}
override suspend fun setMicrophone(enabled: Boolean) { room.localParticipant.setMicrophoneEnabled(enabled) }
override suspend fun setCamera(enabled: Boolean) { room.localParticipant.setCameraEnabled(enabled) }
override suspend fun sendChat(text: String) { room.localParticipant.sendText(text, StreamTextOptions(topic = "lk.chat")) }
override fun disconnect() = room.disconnect()
}
Dependencies: io.livekit:livekit-android:2.29.0. For UI, add io.livekit:livekit-android-compose-components:2.4.2 and pin livekit-android to 2.29.0 explicitly, because the compose artifact pulls in an older version.
Android ignores custom ICE servers without rtcConfig
ConnectOptions(iceServers = …) alone has no effect. Always pass a full rtcConfig as above. Otherwise users behind strict firewalls can't connect.
iOS engine (Swift)¶
import LiveKit
import Shared // your KMP framework
final class SwiftCallEngine: CallEngine {
private let room = Room()
func connect(wsUrl: String, token: String, iceServers: [IceServer]?) async throws {
let servers = (iceServers ?? []).map {
LiveKit.IceServer(urls: $0.urls, username: $0.username, credential: $0.credential)
}
try await room.connect(url: wsUrl, token: token,
connectOptions: ConnectOptions(iceServers: servers))
}
func setMicrophone(enabled: Bool) async throws { try await room.localParticipant.setMicrophone(enabled: enabled) }
func setCamera(enabled: Bool) async throws { try await room.localParticipant.setCamera(enabled: enabled) }
func sendChat(text: String) async throws { _ = try await room.localParticipant.sendText(text, for: "lk.chat") }
func disconnect() { Task { await room.disconnect() } }
}
Add client-sdk-swift 2.17.0 with Swift Package Manager; CocoaPods is deprecated. Add NSCameraUsageDescription and NSMicrophoneUsageDescription to Info.plist, plus the audio background mode.
Check the exact SDK signatures
The snippets show the shape of the integration. Confirm method signatures against the SDK version you pin; the native SDKs change minor APIs between releases.
Invite links¶
Route your invite URL (https://yourapp.example/call/{code}) in the native entry points: an Android intent filter with autoVerify, and an iOS Universal Links entitlement. Parse the code in shared code. See Scheduling & invites.