Skip to content
Skip
2.9k

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"),
]),
]

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" />

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";

The fastest way to add a video room to your app is with LiveKitRoomView. It handles connection, participant display, and media controls:

import SwiftUI
import 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.

For more control over the room lifecycle, use LiveKitRoomManager directly:

import SwiftUI
import 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)")
}
}
}
}

Published Properties:

PropertyTypeDescription
connectionStateLiveKitConnectionStateCurrent state: .disconnected, .connecting, .connected, .reconnecting
participants[LiveKitParticipantInfo]All participants including the local user
roomNameString?The name of the connected room
roomMetadataString?Server-assigned metadata for the room
isCameraEnabledBoolWhether the local camera is active
isMicrophoneEnabledBoolWhether the local microphone is active
isScreenShareEnabledBoolWhether the local screen share is active
isSpeakerphoneEnabledBoolWhether audio is routed to the speakerphone (default true)
isFrontCameraBoolWhether the front-facing camera is selected (default true)
errorMessageString?Error description if connection failed

Callbacks:

PropertyTypeDescription
onDataReceived((String?, Data, String?) -> Void)?Called when a data message arrives. Parameters: sender identity, data, topic.

Methods:

MethodTypeDescription
connect(url:token:)async throwsConnect to a LiveKit server
disconnect()asyncDisconnect from the current room
setCameraEnabled(_:)async throwsToggle the local camera
setMicrophoneEnabled(_:)async throwsToggle the local microphone
setScreenShareEnabled(_:)async throwsToggle local screen sharing
setSpeakerphoneEnabled(_:)Toggle between speakerphone and earpiece audio output
switchCamera()async throwsSwitch between front and back camera
publishData(_:reliability:topic:)async throwsSend data to other participants (reliable or lossy)
updateParticipants()Refresh the participant list from room state

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
}

Connection quality reported per-participant: .unknown, .lost, .poor, .good, .excellent.

Controls data message delivery: .reliable (ordered, guaranteed — like TCP) or .lossy (fast, unordered — like UDP).

// Send a message to all participants
let message = "Hello!".data(using: .utf8)!
try await room.publishData(message, reliability: .reliable, topic: "chat")
// Receive messages
room.onDataReceived = { senderIdentity, data, topic in
if let text = String(data: data, encoding: .utf8) {
print("\(senderIdentity ?? "unknown"): \(text)")
}
}

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)
}
}
}
}

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.

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.

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.