The LiveKit Swift SDK is built with Swift 6 concurrency and data-race safety. Understanding how concurrency works in the SDK is essential for building robust applications.
Concurrency Model
The SDK uses:
async/await for asynchronous operations
actors for synchronization and isolated state
@Sendable types for safe cross-thread data passing
StateSync for thread-safe state management
Background threads for most operations (not @MainActor)
Async/Await Usage
All primary SDK methods are async:
// Connecting to room
try await room. connect ( url : wsURL, token : token)
// Publishing tracks
try await room. localParticipant . setCamera ( enabled : true )
try await room. localParticipant . setMicrophone ( enabled : true )
// Publishing data
try await room. localParticipant . publish ( data : data)
// Disconnecting
await room. disconnect ()
Calling Async Methods
From an async context:
func joinRoom () async throws {
try await room. connect ( url : wsURL, token : token)
try await room. localParticipant . setCamera ( enabled : true )
}
From a synchronous context:
func joinRoomSync () {
Task {
do {
try await room. connect ( url : wsURL, token : token)
} catch {
print ( "Failed to connect: \( error ) " )
}
}
}
From SwiftUI:
struct ContentView : View {
@State private var room = Room ()
var body: some View {
Button ( "Join Room" ) {
Task {
try await room. connect ( url : wsURL, token : token)
}
}
}
}
Thread Safety
State Access
The SDK uses StateSync for thread-safe state access:
// Reading state (thread-safe)
let connectionState = room. connectionState
let participants = room. remoteParticipants
// State is protected internally
let isCameraEnabled = room. localParticipant . isCameraEnabled ()
Delegates and Callbacks
Delegate methods are called on background threads , not the main thread:
class MyRoomDelegate : RoomDelegate {
func room ( _ room : Room, didUpdateConnectionState connectionState : ConnectionState) {
// ⚠️ This is called on a background thread
print ( "Connection state: \( connectionState ) " )
// Update UI on main thread
DispatchQueue. main . async {
self . updateConnectionUI (connectionState)
}
}
}
Or use MainActor:
func room ( _ room : Room, didUpdateConnectionState connectionState : ConnectionState) {
Task { @MainActor in
self . updateConnectionUI (connectionState)
}
}
SwiftUI Integration
For SwiftUI, use @MainActor or dispatch to main:
class RoomViewModel : ObservableObject {
@Published var connectionState: ConnectionState = . disconnected
init () {
room. add ( delegate : self )
}
}
extension RoomViewModel : RoomDelegate {
func room ( _ room : Room, didUpdateConnectionState connectionState : ConnectionState) {
// Update @Published property on main thread
Task { @MainActor in
self . connectionState = connectionState
}
}
}
Actors in the SDK
The SDK uses actors for synchronization:
// SignalClient is an actor
let signalClient = room. signalClient
// Methods on actors are async
await signalClient. send ( message : message)
Actor Isolation
You cannot directly access actor state:
// ❌ This won't compile - actor state requires async access
// let value = actor.someProperty
// ✅ Access actor state via async method
let value = await actor. getSomeProperty ()
Tasks and Cancellation
The SDK uses Task for background work:
// Start a long-running task
let task = Task {
try await room. connect ( url : wsURL, token : token)
}
// Cancel if needed
task. cancel ()
Cooperative Cancellation
Check for cancellation in long-running operations:
func processVideoFrames () async throws {
while ! Task.isCancelled {
try Task. checkCancellation () // Throws if cancelled
// Process frame
await processFrame ()
}
}
Task Cancellables
The SDK uses AnyTaskCancellable for managing tasks:
var cancellable: AnyTaskCancellable ?
func startOperation () {
cancellable = Task {
try await longRunningOperation ()
}. cancellable ()
}
func stopOperation () {
cancellable ? . cancel ()
}
Sendable Types
Most SDK types conform to Sendable for safe cross-thread usage:
// These are safe to pass across threads
let room: Room // @unchecked Sendable
let participant: Participant // @unchecked Sendable
let track: Track // @unchecked Sendable
Custom Delegates
Make your delegates Sendable:
class MyRoomDelegate : RoomDelegate , @unchecked Sendable {
// Implement delegate methods
}
Or mark the delegate as isolated to a global actor:
@MainActor
class MyRoomDelegate : RoomDelegate {
// All methods called on MainActor
}
Common Patterns
Sequential Operations
Perform operations in sequence:
func setupRoom () async throws {
try await room. connect ( url : wsURL, token : token)
try await room. localParticipant . setCamera ( enabled : true )
try await room. localParticipant . setMicrophone ( enabled : true )
print ( "Setup complete" )
}
Parallel Operations
Run independent operations concurrently:
func setupMediaTracks () async throws {
async let camera: Void = room. localParticipant . setCamera ( enabled : true )
async let microphone: Void = room. localParticipant . setMicrophone ( enabled : true )
// Wait for both to complete
try await (camera, microphone)
}
Or use TaskGroup:
func publishMultipleTracks () async throws {
try await withThrowingTaskGroup ( of : Void . self ) { group in
group. addTask {
try await room. localParticipant . setCamera ( enabled : true )
}
group. addTask {
try await room. localParticipant . setMicrophone ( enabled : true )
}
// Wait for all tasks
try await group. waitForAll ()
}
}
Error Handling with Async
func connectWithRetry ( maxAttempts : Int = 3 ) async {
for attempt in 1 ... maxAttempts {
do {
try await room. connect ( url : wsURL, token : token)
print ( "Connected successfully" )
return
} catch {
print ( "Attempt \( attempt ) failed: \( error ) " )
if attempt < maxAttempts {
try ? await Task. sleep ( nanoseconds : 2_000_000_000 )
}
}
}
print ( "Failed to connect after \( maxAttempts ) attempts" )
}
Avoiding Race Conditions
Don’t Access Shared State Without Synchronization
// ❌ Race condition - multiple threads can modify
var isConnected = false
Task {
try await room. connect ( url : wsURL, token : token)
isConnected = true // ⚠️ Not thread-safe
}
Task {
if isConnected { // ⚠️ Not thread-safe
print ( "Connected" )
}
}
// ✅ Use actor for synchronization
actor ConnectionState {
private ( set ) var isConnected = false
func setConnected ( _ connected : Bool ) {
isConnected = connected
}
}
let state = ConnectionState ()
Task {
try await room. connect ( url : wsURL, token : token)
await state. setConnected ( true )
}
Task {
if await state.isConnected {
print ( "Connected" )
}
}
Use SDK State Instead of Caching
// ❌ Don't cache state separately
var cachedParticipants: [RemoteParticipant] = []
// ✅ Use SDK state
let participants = room. remoteParticipants
WebRTC Thread Safety
WebRTC operations must use the LiveKit WebRTC queue:
// Internal SDK code (not for app developers)
DispatchQueue. liveKitWebRTC . sync {
// WebRTC operations here
}
App developers should not interact with WebRTC objects directly. Use the SDK’s public API instead.
Best Practices
Always await SDK methods
Use async let for parallel operations
Handle errors with do-catch
Check Task.isCancelled in long operations
Don’t access SDK internals directly
Use SDK’s public API for state access
Dispatch UI updates to main thread
Make delegates Sendable or @MainActor
Understand that SDK uses actors internally
Await actor method calls
Consider using actors in your own code
Don’t try to synchronously access actor state
SwiftUI Example
Complete SwiftUI example with proper concurrency:
import SwiftUI
import LiveKit
@MainActor
class RoomViewModel : ObservableObject {
@Published var room = Room ()
@Published var connectionState: ConnectionState = . disconnected
@Published var participants: [RemoteParticipant] = []
init () {
room. add ( delegate : self )
}
func connect () async {
do {
try await room. connect ( url : wsURL, token : token)
try await room. localParticipant . setCamera ( enabled : true )
} catch {
print ( "Failed to connect: \( error ) " )
}
}
func disconnect () async {
await room. disconnect ()
}
}
extension RoomViewModel : RoomDelegate {
nonisolated func room ( _ room : Room, didUpdateConnectionState connectionState : ConnectionState) {
Task { @MainActor in
self . connectionState = connectionState
}
}
nonisolated func room ( _ room : Room, participantDidJoin participant : RemoteParticipant) {
Task { @MainActor in
self . participants = Array (room. remoteParticipants . values )
}
}
}
struct RoomView : View {
@StateObject private var viewModel = RoomViewModel ()
var body: some View {
VStack {
Text ( "State: \( viewModel. connectionState ) " )
Button ( "Connect" ) {
Task {
await viewModel. connect ()
}
}
Button ( "Disconnect" ) {
Task {
await viewModel. disconnect ()
}
}
}
}
}
See Also
Swift Concurrency documentation
AGENTS.md in source repository
Support/Async/ directory in SDK
Support/Schedulers/ directory in SDK