Skip to content

SDKs

iOS SDK (Swift) for rooms, chat and broadcast

The VoltKitRTC Swift package: join a VoltKit RTC room from an iPhone app, publish camera and microphone, add chat, go live over WHIP and stream in-app music.

VoltKitRTC is the Swift package for VoltKit RTC. It uses the same names as the JavaScript, Flutter and Android SDKs: StreamClient for rooms, ChatClient for chat only, and BroadcastPublisher for going live. Events arrive as AsyncStreams, and calls are async throws.

ModuleVoltKitRTC (Swift Package Manager)
PlatformsiOS 15+. The media-free core (rooms without media, chat, WHIP over HTTP) also runs on macOS 12+
ToolchainXcode 26 or later
LicenceMIT

Install

In Xcode, choose File → Add Package Dependencies…, enter the package's repository URL, and add the VoltKitRTC product to your app target. In Package.swift:

.product(name: "VoltKitRTC", package: "voltkit-rtc-swift")

The package brings its own WebRTC build, via mediasoup-client-swift. Do not add a second WebRTC package: two WebRTC frameworks in one app means duplicate symbols. import VoltKitRTC re-exports WebRTC, so you get RTCMTLVideoView without a second import.

App setup

Add the permission strings to Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>So people in the room can hear you.</string>
<key>NSCameraUsageDescription</key>
<string>So people in the room can see you.</string>

To keep a live session going in the background, enable the Audio, AirPlay, and Picture in Picture background mode. iOS always stops the camera in the background.

Choose the audio mode once at launch, before creating any client:

// application(_:didFinishLaunchingWithOptions:)
VoltKitMedia.configure(audio: .music)   // or .standard, the default
ModeWhat it isIn-app music
.standardWebRTC's own iOS audio devicenot available
.musicApple's voice processing (echo cancellation, noise suppression) with in-app music mixed inyes

Join a room

Your backend mints a token and hands the app token and wsUrl (see Authentication). The API secret never reaches the device.

import VoltKitRTC

let client = StreamClient(wsUrl: grant.wsUrl, token: grant.token)

// Take streams before connect(): tracks already live arrive during connect().
Task {
    for await track in client.tracks {
        track.videoTrack?.add(remoteView)   // an RTCMTLVideoView; audio plays by itself
    }
}
Task { for await ended in client.trackEnded { removeView(for: ended.producerId) } }
Task { for await reason in client.closed { showEnded(reason) } }   // once, final

try await client.connect()
if client.role.canPublish {
    try await client.publish()            // camera + microphone
    client.localVideoTrack?.add(previewView)
}

try await client.setMicEnabled(false)     // muted for everyone
try await client.setCamEnabled(false)
await client.close()                      // always, when leaving the screen

publish(.audioOnly) publishes voice only. The video defaults suit a phone: 1280×720 at 30 fps, two simulcast layers, and H.264 when the room offers it. switchCamera() flips between the front and back cameras without republishing.

Video quality. 1080p, 720p and 480p are available on every project; video up to 1080p at 30 fps is billed at one rate (see Usage and billing). The preview native SDK still uses its documented capture and encoding defaults; it has not gained the browser/Flutter 0.5.0 preset, frame-rate or screen-share APIs. Set native capture/encoding explicitly if changing quality. The service allows up to 1080p and 60 fps by default, with a shared 25 Mbps incoming safety cap per publisher; explicit operator restrictions still apply. See Video quality.

Events

Each event is an AsyncStream on the client. Iterate it in a Task.

StreamWhen
tracks / trackEndedA track you are subscribed to started or stopped
participantJoined / participantLeft / participantUpdatedSomeone joined, left, or changed their metadata
roleChangedSomeone was promoted or demoted. client.role follows your own role
handRaised / forceMuted / trackMutedHands, moderator mutes, paused tracks
claimChanged / roomMetadataChangedSlots and room metadata changed
messages / messageDeletions / chatMutes / typingChat
usageLimitThe account ran out of minutes or hit a usage limit. See below
connectionStates / reconnectedReconnecting after a network change, and back
closedThe session ended for good: replaced, kicked, roomClosed, gaveUp or left

Chat

Room chat rides the same socket:

Task { for await m in client.messages { append(m) } }      // includes your own
try await client.sendMessage("hello", clientMsgId: "c-1")
try await client.sendMessage("psst", to: "user-7")         // direct message
let older = try await client.getMessages(limit: 50, before: oldestId)

For an audience outside the room, such as a broadcast's viewers, use ChatClient with a chat token. Its wsUrl ends in /ws/chat.

let chat = ChatClient(wsUrl: grant.wsUrl, token: grant.token)
Task { for await m in chat.messages { append(m) } }
let joined = try await chat.connect()     // joined.messages: the latest fifty
try await chat.sendMessage("first!")

Render text as plain text, never as HTML.

Go live (broadcast)

Your backend creates a stream with POST /v1/streams and gives the app publish.whipUrl. Treat it like a password. Viewers play the LL-HLS URL in AVPlayer, so they need nothing from this SDK.

let publisher = BroadcastPublisher(whipUrl: whipUrl)
Task { for await state in publisher.states { show(state) } }  // connecting, live, interrupted, failed, stopped
try await publisher.start()
publisher.setMicEnabled(false)
await publisher.stop()

Broadcast video is H.264 only, so the media server can repackage it without transcoding. When start() is refused, it throws WhipError: isRefused means the URL is expired or for another stream, so fetch a new one.

In-app music

A host can play music from inside the app straight to the audience, without it passing through the speaker and microphone. The music and the microphone have separate switches. See In-app music for the concept.

let song = try await client.publishMusic(url: localFileURL,
                                         options: MusicOptions(volume: 0.8, monitor: true))
try await client.setMusicEnabled(false)   // music off, voice stays on air
try await client.setMicEnabled(false)     // voice off, music stays on air
song.pause(); song.play(); song.seek(to: 30)
await client.stopMusic()

BroadcastPublisher has the same calls. On iOS the music is mixed into the microphone track, because libwebrtc feeds every local audio track from one audio device. Viewers therefore get one track carrying both voice and music. The host's switches are still independent. Music can come from local audio files (MP3, AAC/M4A, WAV, AIFF, CAF, ALAC), but not from DRM-protected tracks or other apps.

Usage limits

When the account runs out of minutes or reaches a daily or monthly limit, every live session is warned first:

Task {
    for await limit in client.usageLimit {      // also on ChatClient
        showBanner("Live ends in \(limit.graceSeconds)s")
    }
}

Unless the account is topped up within the grace period, the server ends the session with WebSocket close code 4003. The SDK does not reconnect, and closed reports .roomClosed. See Usage and billing.

Errors

Calls throw RPCError (code, message, data, isTerminal). WHIP throws WhipError (status, message, isRefused). Act on the code:

do {
    try await client.claim("seat-3")
} catch let error as RPCError where error.code == "slot-taken" {
    showTaken()
}

The server's codes are listed in Errors. The SDK adds disconnected, closed, timeout, permission-denied, no-camera, media-unavailable and music-unavailable (you did not call VoltKitMedia.configure(audio: .music) at launch). No error message ever contains a token or a publish URL.

Help

Email contact@voltkit.in with your SDK version, iOS version and device model.

Last updated . Something unclear or wrong? Tell us.