The iOS SDK is for native Swift apps on iPhone, iPad and Mac. It adds
video and audio calls. It uses the same words as the web SDK — Room,
participants, tracks, and events like participantJoined and
trackSubscribed — written the Swift way, with async/await and
AsyncStream.
It runs on the user's device and holds only a token, never your API
key. Your backend makes the token with the TypeScript,
Python, Go, Java or PHP SDK. Your app fetches it from your own endpoint and
passes on three fields: token, endpoint and iceServers.
Why a backend, and how to write it: the API key can't live in an app, so one small route on your server makes the token. Build your token endpoint has the full route in TypeScript, Python, Go, Java, PHP and Dart, plus the plain HTTP call for any other language. You don't need Node.js unless your backend already uses it.
The shortest version, in your app:
import LivqenoRTC
var options = RoomOptions()
options.iceServers = grant.iceServers // always pass these: they include TURN
let room = Room(options: options)
try await room.connect(token: grant.token, endpoint: grant.endpoint)
try await room.enableMicrophone()
try await room.enableCamera()Without iceServers there's no relay (TURN), and calls on mobile data often fail.
Install
Add the package with Swift Package Manager. It lives in the Livqeno
repository at sdks/ios:
- Xcode: File → Add Package Dependencies… → Add Local… → choose
sdks/ios, then add theLivqenoRTClibrary to your app target. - Package.swift:
.package(path: "path/to/sdks/ios"), then depend on theLivqenoRTCproduct.
Add permissions
Apple won't let an app use the camera or mic unless the app says why, up
front. Add these to your app's Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for calls.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string> <!-- keep call audio alive in the background -->
<string>voip</string> <!-- only if you use CallKit with VoIP pushes -->
</array>On macOS, turn on Camera, Audio Input and Outgoing Connections (Client) under App Sandbox in your target's Signing & Capabilities.
Listen to events
room.events() gives you a stream of events. Start listening before
connect, so you also see the people already in the room:
let room = Room(options: options)
Task {
for await event in room.events() {
switch event {
case let .participantJoined(participant):
addTile(for: participant)
case let .trackSubscribed(track, participant):
attach(track, to: participant) // show video here
case let .trackMuted(kind, participant):
showMutedIcon(kind, participant)
case let .activeSpeakersChanged(speakers):
highlight(speakers.first)
case let .disconnected(reason):
showCallEnded(reason)
default:
break
}
}
}
try await room.connect(token: grant.token, endpoint: grant.endpoint)Prefer a delegate? Set room.delegate and implement
room(_:didEmit:). The event names and what they carry match
RTC Events exactly; there are two extra disconnect reasons,
.tokenRevoked and .replaced (the same person joined from another device).
Show video
let view = LivqenoVideoView() // UIView on iOS, NSView on macOS
view.track = trackLivqenoVideoView also tells the SDK when the view is on screen and how big
it is. Videos you can't see are paused after half a second, and small tiles
get the low-quality layer, which saves the user's data and battery. You can
also control this yourself:
track.setEnabled(false) // pause this video; the server stops sending it
track.setEnabled(true) // resume it straight away
track.setLayer(.low) // pin a quality: .low, .medium, .high or .autoCheck track.subscriptionState when trackSubscriptionStateChanged fires:
.pausedBandwidth means the user's connection can't carry this video right
now, so show a "paused" overlay instead of a frozen frame.
Camera, microphone, screen share
try await room.enableCamera() // front camera
try await room.enableCamera(options: .init(position: .back))
try await room.enableMicrophone()
try await room.enableScreenShare() // this app only
await track.mute() // stop sending, keep the tile
await track.unmute()
try await room.enableCamera(false) // stop publishingThe camera is sent in three quality layers, so people with weak networks still see you. Layers nobody is watching are switched off automatically.
Share the whole screen
enableScreenShare() shares only your app, and only while it's open.
To share the whole screen, iOS needs a Broadcast Upload Extension: a
small extra target in your Xcode project. The SDK ships a ready-made one in
sdks/ios/Templates/BroadcastUploadExtension, with step-by-step setup. It
joins the call as a second participant (for example alice#screen) that
only sends the screen.
Phone-style calls with CallKit
CallKit gives your app the system call screen, lock-screen controls, and
correct behaviour when a normal phone call comes in. CallKitManager wires
it to a room:
let callKit = CallKitManager(configuration: .init(localizedName: "Acme"))
callKit.onAnswer = { _ in try await room.connect(token: grant.token, endpoint: grant.endpoint) }
callKit.onEnd = { _ in await room.leave() }
callKit.onMute = { _, muted in
await room.localParticipant.tracks.first { $0.source == .microphone }?.setMuted(muted)
}
try await callKit.reportIncomingCall(uuid: callId, handle: "Alice", hasVideo: true)Livqeno doesn't ring phones. Your backend sends a VoIP push, and your app reports the call to CallKit when it arrives.
Not using CallKit? Set up audio yourself before the call:
try AudioSessionConfigurator.configureForCall(mode: .videoChat) // speaker on
try AudioSessionConfigurator.setSpeakerOutput(false) // earpieceReconnecting and long calls
You don't need to do anything for a dropped connection. The SDK reconnects
by itself, and usually the call continues without the video ever stopping.
You'll see reconnecting, then reconnected.
Tokens are short-lived. To keep long calls going, give the room a way to get a new one:
options.refreshToken = { try await myBackend.fetchRtcToken() }The SDK calls it before the token runs out and swaps it in without
reconnecting. If you leave it out, you'll get a TOKEN_EXPIRING warning, and
a server that enforces expiry will end the call.
When the device's network changes (Wi-Fi to cellular), call
room.handleNetworkChange() from your NWPathMonitor handler so the SDK
recovers at once instead of waiting.
The SDK never rejoins after .removed or .roomClosed. Those mean your
backend removed this person or closed the room.
Moderation
Your backend can change what someone may do during a call. Read
room.permissions to decide which buttons to show, and listen for
permissionsChanged. If a right was taken away, the SDK has already stopped
those tracks. If one was granted (a live-stream viewer brought on stage),
enableCamera() works straight away.
If your backend mutes someone, they get localTrackMutedByServer and their
track is already muted. They can unmute it themselves.
End-to-end encryption
For rooms created with e2ee: true. The media is encrypted on the device
and only decrypted by the other participants; the server never has the key.
var options = RoomOptions()
options.e2eeKeyProvider = E2EEKeyProvider()
let room = Room(options: options)
try await room.connect(token: grant.token, endpoint: grant.endpoint)
try room.setE2EEPassphrase(sharedSecret) // after connect: the room id salts the key
try await room.enableMicrophone()Everyone in the room must use the same passphrase. iOS, web, Android and
Flutter users can be in the same encrypted call: the keys and the frame
format match byte for byte. room.isE2EEActive tells you media is being
encrypted, and problems arrive as e2eeStateChanged (.decryptFailed
usually means a wrong passphrase, .missingKey that no key is set yet).
If your app hands out keys itself, create the provider with
E2EEKeyProvider(sharedKeyMode: false) and call
room.setKey(participantId:key:keyIndex:) with 32-byte keys (your own
identity's key is the one you send with). room.ratchetKey(participantId:)
moves a key forward.
The SDK never sends unencrypted media into an encrypted room. Without a key
provider, publishing throws RTCError(.e2eeRequired). In an ordinary room a
configured key provider does nothing.
Testing your integration
The SDK's own tests run on a Mac with sdks/ios/scripts/test-macos.sh. To
test calls, the camera, CallKit or screen sharing in your app, you need
Xcode and a real iPhone or iPad. The simulator has no camera and can't
capture the screen.
Next steps
- RTC Events: every event a room sends.
- Signaling protocol: what the SDK says to the server.
- Flutter SDK: if your app is built with Flutter instead.