Supported platforms: macOS (use for platform-agnostic code), macOS Catalyst, iOS, visionOS, tvOS.
Platform destinations: macOS, macOS,variant=Mac Catalyst, iOS Simulator, visionOS Simulator, tvOS Simulator.
# Build
xcodebuild build -scheme LiveKit -destination 'platform=macOS'
# Run tests (requires local server, install via brew install livekit). Data track schema tests
# need participant data blobs, which --dev alone leaves off:
# printf 'enable_participant_data_blob: true\n' > lk.yaml && livekit-server --dev --config lk.yaml
xcodebuild test -scheme LiveKit -only-testing LiveKitCoreTests -destination 'platform=macOS'
# Build benchmarks
cd Benchmarks && swiftly run +xcode swift build
# Run benchmarks (requires local server: livekit-server --dev)
cd Benchmarks && LK_BENCHMARK=1 swiftly run +xcode swift package --disable-sandbox benchmark
# List available simulators for platform-specific builds
xcrun simctl list devicesSources/LiveKit/
├── Core/ # Room, SignalClient, Transport (WebRTC peer connections)
├── Participant/ # LocalParticipant, RemoteParticipant
├── Track/ # LocalAudioTrack, LocalVideoTrack, RemoteTrack, Capturers
├── TrackPublications/ # TrackPublication, LocalTrackPublication, RemoteTrackPublication
├── Audio/ # AudioManager, AudioDeviceModule integration
├── Broadcast/ # Screen sharing via ReplayKit (iOS/macOS)
├── DataStream/ # Reliable/unreliable data channels, byte/text streams
├── E2EE/ # End-to-end encryption
├── Agent/ # AI agent integration (transcription, speech activity)
├── Token/ # TokenSource implementations for auth
├── Types/ # Public types, options, enums
├── Protocols/ # RoomDelegate, ParticipantDelegate, TrackDelegate, etc.
├── Support/
│ ├── Async/ # AsyncCompleter, AsyncTimer, AsyncSequence+Subscribe
│ ├── Sync/ # StateSync, Locks (thread-safe state management)
│ ├── Schedulers/ # QueueActor, SerialRunnerActor (ordered execution)
│ ├── Network/ # WebSocket, HTTP, ConnectivityListener
│ └── Audio/Video/ # Audio converters, device management
├── SwiftUI/ # SwiftUIVideoView, LocalMedia
├── Views/ # VideoView, SampleBufferVideoRenderer
└── Protos/ # Generated protobuf types (excluded from linting)
Key components:
Room- main entry point; manages connection state, participants, and tracks viaStateSyncParticipant- base class forLocalParticipant/RemoteParticipant; holds track publicationsSignalClient- WebSocket connection to LiveKit server; handles signaling protocol as anactorTransport- WebRTCPeerConnectionwrapper; manages ICE, SDP negotiation as anactorStateSync<T>- thread-safe state container with@dynamicMemberLookup; triggersonDidMutatecallbacksMulticastDelegate<T>- weak-reference delegate collection for event broadcasting
Dependencies: LiveKitWebRTC, LiveKitUniFFI. (SwiftProtobuf is test-only — see below.)
The wire protocol is nanopb-based, not SwiftProtobuf: generated C in
Sources/CLiveKitProto, a fixed-cost Swift runtime in Sources/LiveKitNanopb,
and generated facades in Sources/LiveKit/Protos. Every message is the one
generic NanopbMsg<Storage>, so the schema contributes no Swift types;
Livekit_Room is a typealias. Messages are immutable — build with .with { }.
See PROTOCOL.md for the update workflow, the design and its
constraints, memory/concurrency semantics, and the invariants to preserve when
editing. Regenerate with make proto; never edit generated files by hand.
LK_XCFRAMEWORK— set in the generated xcodeproj byscripts/xcframework.swift; that build compilesCLiveKitProto/LiveKitNanopbsources into the framework target directly, so the guards in those files switch tointernal/package import CLiveKitProto(resolved via its modulemap) and dropimport LiveKitNanopb, keeping both out of the emitted.swiftinterfaceLK_BENCHMARK— set when building benchmarks (Benchmarks/); skipsDeviceManager/AudioManagerinit inRoom.initto allow headless benchmark runsLK_SIGNPOSTS— enablesos.signpostinstrumentation inStateSyncfor profiling lock contention in Instruments
WebRTC handles the actual media transport (audio/video/data) between participants. The SDK abstracts WebRTC complexity behind Room, Participant, and Track APIs while LiveKit server coordinates signaling.
Key files:
Core/RTC.swift- factory for creating WebRTC objects (peer connections, tracks, data channels, etc.)Core/Transport.swift- wrapsLKRTCPeerConnection; handles ICE candidates, SDP offer/answerAudio/Manager/-AudioManagerandAudioDeviceModuleintegrationExtensions/RTC*.swift- convenience extensions on WebRTC types
Threading:
- libwebrtc's API objects are proxies: every call — and the release of the last reference — is a
BlockingCallonto WebRTC's signaling/worker/network thread, which can stall. libwebrtc serializes those calls itself; the SDK adds no caller-side serialization for thread safety - Thread-safety is not liveness: a blocking WebRTC call must never run on Swift Concurrency's
width-limited cooperative pool. It runs inside the
@RTCglobal actor (Core/RTC.swift), whose executor is a private dispatch queue that is allowed to block.Transportis@RTC-isolated; other async code wraps such calls inawait RTC.run { }.DispatchQueue.liveKitWebRTCis deprecated public API - Releases and teardown (
deinit,parkChannelRelease) go toRTC.park, which runs them on a concurrent queue — not the serial@RTCexecutor. A release flood is a flood of blocking destructors; routing it through the one serial executor head-of-lines all other RTC work and wedged CI under the sanitizers - Public synchronous APIs that reach WebRTC (the deprecated
create*Trackcreators,AudioManager,volume, renderer attach) block their caller by documented contract — say so in the docstring; new API should beasyncand hop instead - The
@RTCexecutor carries no priority: every hop runs at the queue's.defaultQoS - Exception, by design: the data-channel send path (
sendData,readyState,LKRTCDataBufferconstruction) isnonisolated— it is per-packet and latency-sensitive;sendDatablocks only on the network thread (PROXY_SECONDARY_*),readyStateisBYPASS, the buffer init is a plain byte container - Do not enable the
NonisolatedNonsendingByDefaultupcoming feature (SE-0461) without auditing the@RTCcallers first: under it anonisolated asyncfunction runs on its caller's executor, so theRoommethods@RTC Transportawaits — negotiation, signal sends — would start running on the WebRTC queue. Those would each need@concurrent, which is Swift 6.2 and so needs a#if compiler(>=6.2)guard at the current floor - Completion handlers written inside
@RTCcode must be@Sendable: a non-Sendable closure inherits@RTCisolation, and WebRTC invokes it on its own threads — Swift 6.1-built binaries enforce that in the closure prologue and trap (Xcode 16.4 CI caught exactly this) - WebRTC types are accessed via
internal import LiveKitWebRTCto keep them private from public API
LiveKitCoreTests- unit tests and E2E tests; run on macOS or simulatorsLiveKitAudioTests- audio tests requiring real device with microphone accessLiveKitObjCTests- Obj-C interoperability validationLiveKitTestSupport- test utilities includingwithRoomshelper for multi-participant E2E tests- E2E tests use
withRooms([...]) { rooms in ... }to spawn multiple connected rooms/participants - E2E tests should cover reconnects, partial updates, edge cases, and stress scenarios
@Test(.spec("https://..."))cross-references a test to an upstream spec case via a pinned URL (commit + line anchor)
- Minimum supported Xcode is Xcode 16.3 (Swift 6.1); see Apple's App Store submission requirements
Package.swift(swift-tools-version:6.1) declares the oldest supported version- Keep
Package.swift,LiveKitClient.podspec, and.swiftformat's--swiftversionin sync when changing the minimum - New code should use the latest stable Swift version
- Some constructs require
#if swiftor#if compilerdirectives to support newer-than-minimum toolchains:
#if swift(>=6.2)
private static let playAndRecordOptions: AVAudioSession.CategoryOptions = [.mixWithOthers, .allowBluetoothHFP, .allowBluetoothA2DP, .allowAirPlay]
#else
private static let playAndRecordOptions: AVAudioSession.CategoryOptions = [.mixWithOthers, .allowBluetooth, .allowBluetoothA2DP, .allowAirPlay]
#endif- The SDK uses Swift 6 concurrency and data-race safety
- Most features (excluding UI like
VideoView) perform async work on background threads - Internal entities communicate via
asynccalls,AsyncSequence/AsyncStream, andactorfor synchronization - Network connections and async sources can be modeled as
AsyncSequence - Delegates and closures should only bridge to public APIs (e.g.,
RoomDelegate) actorcan usenonisolatedentry points to integrate withpubliccode- A common pattern is an internal "event loop" to process incoming data in FIFO order
- For synchronous/
nonisolatedAPIs (getters), useStateSyncwith locking and@unchecked Sendable(seeSupport/Sync) - Do not add any new synchronization primitives (locks, queues)
- Minimize lock contention by grouping reads/writes under one
state.mutate { ... }call @unchecked Sendableon a class requires reviewing its internals for synchronization- Avoid
@MainActorfor synchronization of static members in non-UI components - Long-running
Taskrequires cooperative cancellation to avoid memory leaks (e.g.,AsyncSequence.subscribe) - Use
AnyTaskCancellable(viatask.cancellable()) instead of manualTaskmanagement (enforced by SwiftLint) - Fire-and-forget unstructured tasks that may throw must use
Task.discarding/Task.detachedDiscarding; bareTask { try await ... }silently drops errors and is flagged under Swift main-snapshot (#NoUseUnstructuredThrowingTask) - Only
Task.discardingpreserves caller actor isolation in its closure; otherSupport/Async/helpers (Task.retrying,AsyncSerialDelegate.notifyAsync, etc.) run their bodies nonisolated — hop explicitly withawait MainActor.run { ... }when UI-bound work is needed - Use async primitives in
Support/AsyncandSupport/Schedulerswhen operation order matters - Prefer native Swift async/await over
Combinefor new code - Until the minimum supported compiler is Swift 6.3, wrap calls to imported Objective-C completion-handler methods made via their synthesized
asyncoverload in an explicitwithCheckedThrowingContinuation, since the bare auto-bridge hits a mixed Swift 5/6 thunk-coalescing crash (swiftlang/swift#81846) fixed in 6.3
- Crashing consumer code via
fatalError()and similar assertions is not allowed assert()/precondition()should be avoided- For recoverable errors, consider defensive programming first (retry, backoff, graceful failure)
- For non-recoverable errors, propagate with
throwsusingLiveKitErrorwith proper type/code - Anticipate invalid states at compile time using algebraic data types, typestates, etc.
- Unsafe APIs like subscript
[0]should be wrapped and leverage optional?
- Consistency across features is more important than latest syntactic sugar
- Run
swiftlint(see.swiftlint.yml); do not introduce new warnings - Try to remove
// swiftlint:disablein legacy files by refactoring - Deprecation warnings are allowed in public APIs; do not fix them
// Code commentsshould be used sparingly; prefer better naming/structuring- Do not add trivial "what" comments like
// Here is the change /// Docstringsfor every public API using Swift markdown (- Note,- Warning,- SeeAlso, etc.)- Add short code examples for new APIs to the entry point (e.g.,
Roomclass) Loggablelogs use.debugby default;.warning/.erroronly for consumer-facing issues- Remove
_privateFieldsnaming inconsistencies when touching surrounding code
- If an object conforms to
ObservableObject, make sure changes are published - It may require manually calling
objectWillChange.send()combined withStateSyncon@MainActor
- Public APIs should support Obj-C with
@objcat class level - This restricts Swift types (no enums with associated values, structs, async primitives)
- Internal/private APIs should not support Obj-C unless required; use Swift's type system
- If Obj-C leads to awkward patterns, wrap Swift in additional layers rather than sacrificing Swift APIs
<skills_system priority="1">
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.How to use skills:
- Invoke:
npx openskills read <skill-name>(run in your shell)- For multiple:
npx openskills read skill-one,skill-two
- For multiple:
- The skill content will load with detailed instructions on how to complete the task
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
- Each skill invocation is stateless
<available_skills>
swift-concurrency 'Diagnose data races, convert callback-based code to async/await, implement actor isolation patterns, resolve Sendable conformance issues, and guide Swift 6 migration. Use when developers mention: (1) Swift Concurrency, async/await, actors, or tasks, (2) "use Swift Concurrency" or "modern concurrency patterns", (3) migrating to Swift 6, (4) data races or thread safety issues, (5) refactoring closures to async/await, (6) @MainActor, Sendable, or actor isolation, (7) concurrent code architecture or performance optimization, (8) concurrency-related linter warnings (SwiftLint or similar; e.g. async_without_await, Sendable/actor isolation/MainActor lint).' global swift-testing-expert 'Expert guidance for Swift Testing: test structure, #expect/#require macros, traits and tags, parameterized tests, test plans, parallel execution, async waiting patterns, and XCTest migration. Use when writing new Swift tests, modernizing XCTest suites, debugging flaky tests, or improving test quality and maintainability in Apple-platform or Swift server projects.' global</available_skills>
</skills_system>