WebRTC (LiveKit)
SkipLiveKit provides a cross-platform LiveKit ↗ integration for Skip apps. It wraps the LiveKit Swift SDK ↗ on iOS and the LiveKit Android SDK ↗ on Android behind a unified SwiftUI API, so you can add real-time video and audio rooms to your app without writing any platform-specific code.
Add SkipLiveKit to your Package.swift:
dependencies: [ .package(url: "https://source.skip.tools/skip-livekit.git", "0.0.0"..<"2.0.0"),],targets: [ .target(name: "MyApp", dependencies: [ .product(name: "SkipLiveKit", package: "skip-livekit"), ]),]Android Permissions
Section titled “Android Permissions”Your AndroidManifest.xml must include the following permissions for camera, microphone, and network access:
<uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />iOS Permissions
Section titled “iOS Permissions”Add these keys to your Darwin/AppName.xcconfig:
INFOPLIST_KEY_NSCameraUsageDescription = "This app needs camera access for video calls";INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs microphone access for audio calls";Quick Start
Section titled “Quick Start”The fastest way to add a video room to your app is with LiveKitRoomView. It handles connection, participant display, and media controls:
import SwiftUIimport SkipLiveKit
struct CallView: View { var body: some View { LiveKitRoomView( url: "wss://your-server.livekit.cloud", token: "your-jwt-access-token" ) }}This renders a participant grid with controls for camera, microphone, screen sharing, camera flip, speakerphone, and hang-up. It connects automatically when the view appears. Participant tiles show connection quality indicators, speaking highlights, and screen share status.
Using LiveKitRoomManager
Section titled “Using LiveKitRoomManager”For more control over the room lifecycle, use LiveKitRoomManager directly:
import SwiftUIimport SkipLiveKit
struct MyRoomView: View { @StateObject var room = LiveKitRoomManager()
var body: some View { VStack { Text("Room: \(room.roomName ?? "Not connected")") Text("State: \(room.connectionState.rawValue)") Text("Participants: \(room.participants.count)")
if room.connectionState == .connected { HStack { Button(room.isMicrophoneEnabled ? "Mute" : "Unmute") { Task { try? await room.setMicrophoneEnabled(!room.isMicrophoneEnabled) } } Button(room.isCameraEnabled ? "Camera Off" : "Camera On") { Task { try? await room.setCameraEnabled(!room.isCameraEnabled) } } Button("Leave") { Task { await room.disconnect() } } } } } .task { do { try await room.connect( url: "wss://your-server.livekit.cloud", token: "your-jwt-access-token" ) } catch { print("Connection failed: \(error)") } } }}LiveKitRoomManager API
Section titled “LiveKitRoomManager API”Published Properties:
| Property | Type | Description |
|---|---|---|
connectionState | LiveKitConnectionState | Current state: .disconnected, .connecting, .connected, .reconnecting |
participants | [LiveKitParticipantInfo] | All participants including the local user |
roomName | String? | The name of the connected room |
roomMetadata | String? | Server-assigned metadata for the room |
isCameraEnabled | Bool | Whether the local camera is active |
isMicrophoneEnabled | Bool | Whether the local microphone is active |
isScreenShareEnabled | Bool | Whether the local screen share is active |
isSpeakerphoneEnabled | Bool | Whether audio is routed to the speakerphone (default true) |
isFrontCamera | Bool | Whether the front-facing camera is selected (default true) |
errorMessage | String? | Error description if connection failed |
Callbacks:
| Property | Type | Description |
|---|---|---|
onDataReceived | ((String?, Data, String?) -> Void)? | Called when a data message arrives. Parameters: sender identity, data, topic. |
Methods:
| Method | Type | Description |
|---|---|---|
connect(url:token:) | async throws | Connect to a LiveKit server |
disconnect() | async | Disconnect from the current room |
setCameraEnabled(_:) | async throws | Toggle the local camera |
setMicrophoneEnabled(_:) | async throws | Toggle the local microphone |
setScreenShareEnabled(_:) | async throws | Toggle local screen sharing |
setSpeakerphoneEnabled(_:) | Toggle between speakerphone and earpiece audio output | |
switchCamera() | async throws | Switch between front and back camera |
publishData(_:reliability:topic:) | async throws | Send data to other participants (reliable or lossy) |
updateParticipants() | Refresh the participant list from room state |
LiveKitParticipantInfo
Section titled “LiveKitParticipantInfo”Each participant is represented as a simple value type:
public struct LiveKitParticipantInfo: Identifiable { public let id: String public let identity: String public let name: String public let metadata: String? public let isSpeaking: Bool public let audioLevel: Float public let connectionQuality: LiveKitConnectionQuality public let isCameraEnabled: Bool public let isMicrophoneEnabled: Bool public let isScreenShareEnabled: Bool public let isLocal: Bool}LiveKitConnectionQuality
Section titled “LiveKitConnectionQuality”Connection quality reported per-participant: .unknown, .lost, .poor, .good, .excellent.
LiveKitDataReliability
Section titled “LiveKitDataReliability”Controls data message delivery: .reliable (ordered, guaranteed — like TCP) or .lossy (fast, unordered — like UDP).
Sending and Receiving Data
Section titled “Sending and Receiving Data”// Send a message to all participantslet message = "Hello!".data(using: .utf8)!try await room.publishData(message, reliability: .reliable, topic: "chat")
// Receive messagesroom.onDataReceived = { senderIdentity, data, topic in if let text = String(data: data, encoding: .utf8) { print("\(senderIdentity ?? "unknown"): \(text)") }}Development and Testing View
Section titled “Development and Testing View”LiveKitConnectView provides a simple form for entering a server URL and token, useful during development:
import SkipLiveKit
struct DevView: View { var body: some View { NavigationStack { LiveKitConnectView { url, token in LiveKitRoomView(url: url, token: token) } } }}Platform Implementation
Section titled “Platform Implementation”SkipLiveKit is a Skip Lite module. The Swift source is transpiled to Kotlin for Android.
On iOS, SkipLiveKit uses the LiveKit Swift SDK ↗ (client-sdk-swift v2.3+). Room management, participant tracking, and media control all go through the Swift Room class.
On Android, the transpiled Kotlin code uses the LiveKit Android SDK ↗ (livekit-android v2.24+). The io.livekit.android.LiveKit.create(context) factory creates a Room instance, and all participant and media APIs are accessed through the Android SDK’s Kotlin API.
Platform differences in property types (such as Kotlin inline classes for participant IDs) are handled internally so the public API is identical on both platforms.
Building
Section titled “Building”This project is a free Swift Package Manager module that uses the Skip plugin to transpile Swift into Kotlin.
Building the module requires that Skip be installed using
Homebrew ↗ with brew install skiptools/skip/skip.
This will also install the necessary build prerequisites:
Kotlin, Gradle, and the Android build tools.
Testing
Section titled “Testing”The module can be tested using the standard swift test command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with skip test,
which will output a table of the test results for both platforms.